def threshold_mean_std(arr, std_steps=(-2, -1, 0, 1, 2), mean_steps=1): """ Calculate threshold from mean and standard deviations. This is the threshold value at which X% (= value) of the data is smaller than the threshold. Args: arr (np.ndarray): The input array. std_steps (Iterable[int|float]): The st.dev. multiplication step(s). These are usually values between -2 and 2. mean_steps (Iterable[int|float]): The mean multiplication step(s). This is usually set to 1. Returns: result (tuple[float]): the calculated thresholds. """ mean = np.nanmean(arr) std = np.nanstd(arr) min_val = np.min(arr) max_val = np.max(arr) mean_steps = fc.auto_repeat(mean_steps, 1) std_steps = fc.auto_repeat(std_steps, 1) return tuple( mean * mean_step + std * std_step for mean_step, std_step in itertools.product(mean_steps, std_steps) if min_val <= mean * mean_step + std * std_step <= max_val)
def calc_ti_to_td(ti, tr_seq, tr_gre, n_gre, center_k=0.5, check=True): """ Compute delay times T_D from sampling times T_I. Args: ti (Iterable[float]): The sampling times in time units. tr_seq (float): The repetition time of the sequence in time units. tr_gre (float|Iterable[float]): The repetition times in time units. If Iterable, must match the length of `ti`. n_gre (int|Iterable[int]): The number of k-space lines in #. If Iterable, must match the length of `ti`. center_k (float|Iterable[float]): The position of the k-space center. Value(s) must be in the [0, 1] range. If Iterable, must match the length of `ti` check (bool): Check if results are valid. Returns: td (tuple[float]): The delay times in time units. Matches the length of `ti` plus one. Raises: ValueError: If resulting `td` values are negative. Examples: >>> ti = [500, 2500] >>> tr_seq = 5000 >>> tr_gre = [2, 5] >>> n_gre = [50, 100] >>> center_k = [0.8, 0.2] >>> calc_ti_to_td(ti, tr_seq, tr_gre, n_gre, center_k) (420.0, 1880.0, 2100.0) >>> calc_ti_to_td(ti, tr_seq, tr_gre, n_gre) (450.0, 1700.0, 2250.0) >>> ti = [1000, 3000] >>> tr_seq = 6000 >>> tr_gre = 20 >>> n_gre = 100 >>> center_k = [0.5, 0.5] >>> calc_ti_to_td(ti, tr_seq, tr_gre, n_gre, center_k) (0.0, 0.0, 2000.0) """ n_ti = len(ti) tr_gre = np.array(fc.auto_repeat(tr_gre, n_ti, check=True)) n_gre = np.array(fc.auto_repeat(n_gre, n_ti, check=True)) center_k = np.array(fc.auto_repeat(center_k, n_ti, check=True)) tr_block = tr_gre * n_gre before_t = tr_block * center_k after_t = tr_block * (1 - center_k) inner_t_block = before_t[1:] + after_t[:-1] # print(tr_block, before_t, after_t, inner_t_block) # DEBUG td = (((ti[0] - before_t[0]), ) + tuple(np.diff(ti) - inner_t_block) + ((tr_seq - ti[-1] - after_t[-1]), )) # internal checksum assert (np.sum(td) + np.sum(tr_block) == tr_seq) if check: if any(x < 0.0 for x in td): raise ValueError('Negative delay times detected: {}'.format(td)) return td
def stacked_circular_loops_alt( radius_factors=1, distance_factors=None, current_factors=1, position=0.5, normal=(0., 1., 0.), radius=0.25, current=1, n_loops=None): """ Generate parallel circular loops (using alternate input). This is equivalent to `pymrt.extras.em_fields.stacked_circular_loops()` except that the inputs are formulated differently (but are otherwise equivalent). Args: radius_factors (Iterable[int|float]): The factors for the radiuses. distance_factors (Iterable[int|float]): The factors for the distances. current_factors (Iterable[int|float]): The factors for the currents. position (float|Iterable[float]): The position of the center. Values are relative to the lowest edge. normal (Iterable[int|float]): The orientation (normal) of the loop. This is a 2D or 3D unit vector. The any magnitude information will be lost. radius (int|float): The base value for the radius. This is also used to compute the distances. current (int|float): The base value for the current. n_loops (int|None): The number of loops. If None, this is inferred from the other parameters, but at least one of `radiuses`, `currents` must be iterable. Returns: circ_loops (list[CircularLoop]): The circular loops. """ if n_loops is None: n_loops = fc.combine_iter_len((radius_factors, current_factors)) radius_factors = fc.auto_repeat( radius_factors, n_loops, check=True) if distance_factors is None: distance_factors = 2 / n_loops distance_factors = fc.auto_repeat( distance_factors, n_loops - 1, check=True) current_factors = fc.auto_repeat( current_factors, n_loops, check=True) distances = [k * radius for k in distance_factors] positions = fcn.distances2displacements(distances) return stacked_circular_loops( [k * radius for k in radius_factors], positions, [k * current for k in current_factors], position, normal, n_loops)
def cylinder_with_infinite_wires( n_wires=8, currents=1, position=0.5, diameters=0.6, angle=0.0, angle_offset=0.0, direction=(0., 0., 1.)): """ Generate infinite wires along the lateral surface of a cylinder. The cylinder may have circular or elliptical basis. Args: n_wires (int): The number of wires. currents (int|float|Iterable[int|float]): The currents in the loops. position (float|Iterable[float]): The position of the center. Values are relative to the lowest edge. diameters (int|float|Iterable[int|float]): The axes / diameter. If int or float, this is the diameter of the circular cylinder. If Iterable, size must be 2, and these are the major and minor axes of the elliptical cylinder base. angle (int|float): The rotation of the cylinder basis in deg. angle_offset (int|float): The phase offset of the angle in deg. direction (Iterable[int|float]: The direction of the cylinder. Must have size 3. Returns: infinite_wires (list[InfiniteWires]): The infinite wires. """ n_dim = 3 if n_wires is None: n_wires = fc.combine_iter_len((currents,)) position = np.array(fc.auto_repeat(position, n_dim, check=True)) diameters = fc.auto_repeat(diameters, 2, check=True) currents = fc.auto_repeat(currents, n_wires, check=True) orientation = np.array((0., 0., 1.)) rot_matrix = np.dot( fcn.rotation_3d_from_vector(orientation, angle), fcn.rotation_3d_from_vectors(orientation, direction)) a, b = [x / 2.0 for x in diameters] positions = [ position + np.dot( rot_matrix, np.array([a * np.cos(phi), b * np.sin(phi), 0])) for phi in fcn.angles_in_ellipse( n_wires, a, b, np.deg2rad(angle_offset))] infinite_wires = [ InfiniteWire(position_, direction, current) for position_, current in zip(positions, currents)] return infinite_wires
def crossing_circular_loops( position=0.5, direction=(0., 0., 1.), radiuses=0.4, angles=None, currents=1, n_loops=None): """ Generate circular loops sharing the same diameter. Args: position (float|Iterable[float]): The position of the center. Values are relative to the lowest edge. direction (Iterable[int|float]: The direction of the shared diameter. Must have size 3. radiuses (int|float|Iterable[int|float]): The loop radiuses. If int or float, the same value is used for all loops. If Iterable, its size must be `n_loops`. angles (int|float|Iterable[int|float]|None): The loop angles in deg. This is the tilting of the circular loop around `direction`. If int or float, a single loop is assumed. If Iterable, its size must be `n_loops`. If None, the angles are linearly distributed in the [0, 180) range, resulting in equally angularly spaced loops. currents (int|float|Iterable[int|float]): The currents in the loops. n_loops (int|None): The number of loops. If None, this is inferred from the other parameters, but at least one of `radiuses`, `angles`, `currents` must be iterable. Returns: circ_loops (list[CircularLoop]): The circular loops. """ n_dim = 3 position = np.array(fc.auto_repeat(position, n_dim, check=True)) if not n_loops: n_loops = fc.combine_iter_len((angles, radiuses, currents)) angles = np.linspace(0.0, 180.0, n_loops, False) radiuses = fc.auto_repeat(radiuses, n_loops, check=True) currents = fc.auto_repeat(currents, n_loops, check=True) orientation = (0., 0., 1.) rot_matrix = fcn.rotation_3d_from_vectors(orientation, direction) normal = np.dot(rot_matrix, (0., 1., 0.)) normals = [ np.dot(fcn.rotation_3d_from_vector(orientation, angle), normal) for angle in angles] circ_loops = [ CircularLoop(radius, position, normal, current) for radius, normal, current in zip(radiuses, normals, currents)] return circ_loops
def __init__(self, preps, tes=None, tr=None, fa=None, *_args, **_kws): """ Args: preps (): tes (): tr (): *_args (): **_kws (): """ MultiGradEchoSteadyState.__init__(self, None, None, *_args, **_kws) # fix preps if fa is None: fa = self.pulses[self._idx['PulseExc']].flip_angle len_labels = len(self.get_prep_labels()) self.preps = [] optional_preps = ((fa, 'FlipAngle', False), (tr, 'TR', False), (tes, 'TEs', True)) for prep in preps: prep = list(prep) + [None] * (len_labels - len(prep)) for param, label, is_seq in optional_preps: i = self.get_prep_labels().index(label) if param is not None and prep[i] is None: prep[i] = param if is_seq: prep[i] = fc.auto_repeat(prep[i], 1, False, False) assert (all(prep_val is not None for prep_val in prep)) self.preps.append(prep) idx = self.get_unique_pulses(('MagnetizationPreparation', )) if hasattr(self, '_idx'): self._idx.update(idx) else: self._idx = idx
def clip_range(arr, interval, out_values=None): """ Set values outside the specified interval to constant. Similar masking patters could be obtained with `label_thresholds()` or `threshold_to_mask()`. Args: arr (np.ndarray): The input array. interval (Iterable[int|float]): The values interval. Must contain 2 items: (t1, t2) Values outside this range are set according to `out_values`. out_values (int|float|Iterable[int|float]|None): The replacing values. If int or float, values outside the (t1, t2) range are replaced with `out_values`. If Iterable, must contain 2 items: (v1, v2), and values below `t1` are replaced with `v1`, while values above `t2` are replaced with `v2`. If None, uses v1 = t1 and v2 = t2: values below `t1` are replaced with `t1`, while values above `t2` are replaced with `t2`. Returns: arr (np.ndarray): The clipped array. """ t1, t2 = interval if out_values is None: out_values = interval out_values = fc.auto_repeat(out_values, 2, check=True) v1, v2 = out_values arr[arr < t1] = v1 arr[arr > t2] = v2 return arr
def __init__( self, size=0.5, center=(0.5, 0.5, 0.5), normal=(0., 0., 1.), current=1.): """ Define the rectangular loop. Args: size (int|float|Iterable[int|float]): The size(s) of the rectangle. If int or float, the two sides of the rectangle are equal. If Iterable, its size must be 2. Units are not specified. center (Iterable[int|float]): The center of the loop. This can be a 2D or 3D vector. Units are not specified. normal (Iterable[int|float]): The orientation (normal) of the loop. This is a 2D or 3D unit vector. The any magnitude information will be lost. current (int|float): The current circulating in the loop. Units are not specified. """ self.center = _to_3d(np.array(center)) self.normal = _to_3d(fcn.normalize(normal)) self.radius = fc.auto_repeat(size, 2, check=True) self.current = current
def dphs_to_phs(dphs_arr, tis, phs0_arr=0, time_units='ms'): """ Calculate the phase variation from phase data. Args: dphs_arr (np.ndarray): The input array in rad. The sampling time Ti varies in the last dimension. tis (Iterable|int|float): The sampling times Ti in time units. The number of points will match the last shape size of `phs_arr`. phs0_arr (np.ndarray|int|float): The initial phase offset. If int or float, a constant offset is used. time_units (str|float|int): Units of measurement of Ti. If str, any valid SI time unit (e.g. `ms`, `ns`) will be accepted. If int or float, the conversion factor will be multiplied to `ti`. Returns: phs_arr (np.ndarray): The phase array in rad. """ units_factor = 1 if isinstance(time_units, str) and 's' in time_units: prefix, _ = time_units.split('s', 1) units_factor = fc.prefix_to_factor(prefix) elif isinstance(time_units, (int, float)): units_factor = time_units else: warnings.warn(fmtm('Invalid units `{time_units}`. Ignored.')) shape = dphs_arr.shape tis = np.array(fc.auto_repeat(tis, 1)) * units_factor tis = tis.reshape((1, ) * len(shape) + (-1, )) dphs_arr = dphs_arr.reshape(shape + (1, )) return dphs_arr * tis + phs0_arr
def sn_split_otsu(arr, corrections=(1.0, 0.2)): """ Separate signal from noise using the Otsu threshold. Args: arr (np.ndarray): The input array. corrections (int|float|Iterable[int|float]: The correction factors. If value is 1, no correction is performed. If int or float, the Otsu threshold is corrected (multiplied) by the corresponding factor before thresholding. If Iterable, the first correction is used to estimate the signal, while the second correction is used to estimate the noise. At most two values are accepted. When the two values are not identical some values may be ignored or counted both in signal and in noise. Returns: result (tuple[np.ndarray]): The tuple contains: - signal_arr: The signal array. - noise_arr: The noise array. """ corrections = fc.auto_repeat(corrections, 2, check=True) otsu = mrt.segmentation.threshold_otsu(arr) signal_mask = arr > otsu * corrections[0] noise_mask = arr <= otsu * corrections[1] signal_arr = arr[signal_mask] noise_arr = arr[noise_mask] return signal_arr, noise_arr
def sn_split_percentile(arr, thresholds=(0.75, 0.25)): """ Separate signal from noise using the percentile threshold(s). Args: arr (np.ndarray): The input array. thresholds (int|float|Iterable[int|float]: The percentile values. Values must be in the [0, 1] range. If int or float, values above are considered signal, and below or equal ar considered noise. If Iterable, values above the first percentile threshold are considered signals, while values below the second percentile threshold are considered noise. At most two values are accepted. When the two values are not identical some values may be ignored or counted both in signal and in noise. Returns: result (tuple[np.ndarray]): The tuple contains: - signal_arr: The signal array. - noise_arr: The noise array. See Also: segmentation.threshold_percentile() """ thresholds = fc.auto_repeat(thresholds, 2, check=True) signal_threshold, noise_threshold = \ mrt.segmentation.threshold_percentile(arr, thresholds) signal_mask = arr > signal_threshold noise_mask = arr <= noise_threshold signal_arr = arr[signal_mask] noise_arr = arr[noise_mask] return signal_arr, noise_arr
def cyclic_padding_tile(arr, shape, offsets): """ Generate a cyclical padding of an array to a given shape with offsets. Implemented using single element loops. Args: arr (np.ndarray): The input array. shape (int|Iterable[int]): The output shape. If int, a shape matching the input dimension is generated. offsets (int|float|Iterable[int|float]): The input offset. The input is shifted by the specified offset before padding. If int or float, the same offset is applied to all dimensions. If float, the offset is scaled to the difference between the input shape and the output shape. Returns: result (np.ndarray): The cyclic padded array of given shape. Examples: >>> arr = fc.extra.arange_nd((2, 3)) + 1 >>> print(arr) [[1 2 3] [4 5 6]] >>> print(cyclic_padding_tile(arr, (4, 5), (1, 1))) [[5 6 4 5 6] [2 3 1 2 3] [5 6 4 5 6] [2 3 1 2 3]] """ shape = fc.auto_repeat(shape, arr.ndim, check=True) offsets = fc.auto_repeat(offsets, arr.ndim, check=True) offsets = tuple( (int(round((new_dim - dim) * offset)) if isinstance(offset, float) else offset) % dim for dim, new_dim, offset in zip(arr.shape, shape, offsets)) assert (arr.ndim == len(shape) == len(offsets)) tiling = tuple(new_dim // dim + (1 if new_dim % dim else 0) + (1 if offset else 0) for offset, dim, new_dim in zip(offsets, arr.shape, shape)) result = np.tile(arr, tiling) slicing = tuple( slice(offset, offset + new_dim) for offset, new_dim in zip(offsets, shape)) return result[slicing]
def symmetric_padding_loops(arr, shape, offsets): """ Generate a symmetrical padding of an array to a given shape with offsets. Implemented using single element loops. Args: arr (np.ndarray): The input array. shape (int|Iterable[int]): The output shape. If int, a shape matching the input dimension is generated. offsets (int|float|Iterable[int|float]): The input offset. The input is shifted by the specified offset before padding. If int or float, the same offset is applied to all dimensions. If float, the offset is scaled to the difference between the input shape and the output shape. Returns: result (np.ndarray): The symmetric padded array of given shape. Examples: >>> arr = fc.extra.arange_nd((2, 3)) + 1 >>> print(arr) [[1 2 3] [4 5 6]] >>> print(symmetric_padding_loops(arr, (4, 5), (-1, -1))) [[6 6 5 4 4] [6 6 5 4 4] [3 3 2 1 1] [3 3 2 1 1]] """ shape = fc.auto_repeat(shape, arr.ndim, check=True) offsets = fc.auto_repeat(offsets, arr.ndim, check=True) offsets = tuple( (int(round((new_dim - dim) * offset)) if isinstance(offset, float) else offset) % dim for dim, new_dim, offset in zip(arr.shape, shape, offsets)) assert (arr.ndim == len(shape) == len(offsets)) result = np.zeros(shape, dtype=arr.dtype) for ij in itertools.product(*tuple(range(dim) for dim in result.shape)): slicing = tuple( (i + offset) % dim if not ((i + offset) // dim % 2) else (dim - 1 - i - offset) % dim for i, offset, dim in zip(ij, offsets, arr.shape)) result[ij] = arr[slicing] return result
def shepp_logan_like(values=1.0): """ The Shepp-Logan phantom with custom intensity values. Args: values (int|float|Iterable[int|float]): Intensities of the ellipses. If Iterable, must have a length of 10. Returns: geom_shapes (Iterable[Iterable]): The geometric specifications. These are of the form: (intensity, [name, *_args], shift, angles, position). See the `geom_shapes` parameter of `raster_geometry.multi_render()` for more details. References: - Shepp, L. A., and B. F. Logan. “The Fourier Reconstruction of a Head Section.” IEEE Transactions on Nuclear Science 21, no. 3 (June 1974): 21–43. https://doi.org/10.1109/TNS.1974.6499235. Notes: - This is implemented based on `raster_geometry.nd_superellipsoid()` and therefore the sizes and the inner positions are halved, while angular values are defined differently compared to the reference paper. """ geom_shapes = [ [['ellipsoid', [0.3450, 0.4600]], [+0.0000, +0.0000], [+000.], 0.5], [['ellipsoid', [0.3312, 0.4370]], [+0.0000, -0.0092], [+000.], 0.5], # angle: 72 = 90 - 18 [['ellipsoid', [0.1550, 0.0550]], [+0.1100, +0.0000], [+072.], 0.5], # angle: 108 = 90 + 18 [['ellipsoid', [0.2050, 0.0800]], [-0.1100, +0.0000], [+108.], 0.5], [['ellipsoid', [0.1250, 0.1050]], [+0.0000, +0.1750], [+000.], 0.5], [['ellipsoid', [0.0230, 0.0230]], [+0.0000, +0.0500], [+000.], 0.5], [['ellipsoid', [0.0230, 0.0230]], [+0.0000, -0.0500], [+000.], 0.5], [['ellipsoid', [0.0230, 0.0115]], [-0.0400, -0.3025], [+000.], 0.5], [['ellipsoid', [0.0115, 0.0115]], [+0.0000, -0.3025], [+000.], 0.5], [['ellipsoid', [0.0115, 0.0230]], [+0.0300, -0.3025], [+000.], 0.5], ] fc.auto_repeat(values, len(geom_shapes), False, True) geom_shapes = [([value] + geom_shape) for value, geom_shape in zip(values, geom_shapes)] return geom_shapes
def unwrap_1d_iter(arr, axes=None, denoising=sp.ndimage.gaussian_filter, denoising_kws=(('sigma', 1.0), ), congruences=16, discont=lambda x: x >= 3 * np.pi / 2, discont_mask=None): """ Iterate one-dimensional unwrapping over all directions. This is effective in multi-dimensional unwrapping provided that the image is sufficiently smooth. This can be achieved by numerically achieved by up-sampling followed by down-sampling. Args: arr (np.ndarray): The wrapped phase array. axes (Iterable[int]|int|None): The dimensions along which to unwrap. If Int, unwrapping in a single dimension is performed. If None, unwrapping is performed in all dimensions from 0 to -1. denoising (callable|None): The denoising function. If callable, must have the following signature: denoising(np.ndarray, ...) -> np.ndarray. It is applied to the real and imaginary part of `np.exp(1j * arr)` separately, using `fcn.filter_cx()` and then converted back to a phase with `np.angle()` before applying the unwrapping. denoising_kws (Mappable|None): Keyword arguments. These are passed to the function specified in `denoising`. If Iterable, must be convertible to a dictionary. If None, no keyword arguments will be passed. congruences (int): The number of congruence values to test. See `pymrt.recipes.phs.congruence_correction()` for more info. discont (callable): The discontinuity condition. See `pymrt.recipes.phs.congruence_correction()` for more info. discont_mask (np.ndarray[bool]|None): The discontinuity mask. See `pymrt.recipes.phs.congruence_correction()` for more info. Returns: arr (np.ndarray): The unwrapped phase array. """ u_arr = arr.copy() if callable(denoising): denoising_kws = dict(denoising_kws) \ if denoising_kws is not None else {} u_arr = np.angle( fcn.filter_cx(np.exp(1j * u_arr), denoising, (), denoising_kws)) if axes is None: axes = tuple(range(arr.ndim)) axes = fc.auto_repeat(axes, 1) for i in axes: u_arr = np.unwrap(u_arr, axis=i) u_arr = fix_congruence(arr, u_arr, 2 * np.pi, congruences, discont, discont_mask) return u_arr
def sampling_mask(traj, shape, factors=1, fit=True): """ Generate a sampling mask of given shape from a trajectory. Args: traj (np.ndarray): The coordinates of the trajectory. The shape is: (n_dim, n_points). shape (Iterable[int]): The shape of the array. factors (int|float|Iterable[int|float]): The scaling factor(s). The If int or float, the same factor is used for all dimensions. If Iterable, must match the dimensions of the trajectory. fit (bool): Fit the entire trajectory within the shape. Returns: result (np.ndarray[bool]): The sampling mask. This can be applied to any `nd.array` with a matching shape to sample the specified trajectory. Examples: >>> traj = np.array(((0, 0), (1, 1), (2, 2))).T >>> print(sampling_mask(traj, (3, 3))) [[ True False False] [False True False] [False False True]] >>> arr = np.arange(3 * 3).reshape((3, 3)) + 1 >>> print(arr * sampling_mask(traj, arr.shape)) [[1 0 0] [0 5 0] [0 0 9]] >>> print(sampling_mask(traj, (3, 3), factors=2)) [[ True False False False False False] [False False False False False False] [False False True False False False] [False False False False False False] [False False False False False False] [False False False False False True]] >>> print(sampling_mask(traj, (3, 3), factors=3).astype(int)) [[1 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0] [0 0 0 0 1 0 0 0 0] [0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 1]] """ n_dim = len(shape) factors = fc.auto_repeat(factors, n_dim, False, True) shape = tuple(int(size * factor) for size, factor in zip(shape, factors)) if fit: traj = reframe(traj, tuple((0, size - 1) for size in shape)) result = np.zeros(shape, dtype=bool) result[tuple(x for x in np.round(traj).astype(int))] = True return result
def cyclic_padding_slicing(arr, shape, offsets): """ Generate a cyclical padding of an array to a given shape with offsets. Implemented using slicing. Args: arr (np.ndarray): The input array. shape (int|Iterable[int]): The output shape. If int, a shape matching the input dimension is generated. offsets (int|float|Iterable[int|float]): The input offset. The input is shifted by the specified offset before padding. If int or float, the same offset is applied to all dimensions. If float, the offset is scaled to the difference between the input shape and the output shape. Returns: result (np.ndarray): The cyclic padded array of given shape. Examples: >>> arr = fc.extra.arange_nd((2, 3)) + 1 >>> print(arr) [[1 2 3] [4 5 6]] >>> print(cyclic_padding_slicing(arr, (4, 5), (1, 1))) [[5 6 4 5 6] [2 3 1 2 3] [5 6 4 5 6] [2 3 1 2 3]] """ offsets = fc.auto_repeat(offsets, arr.ndim, check=True) offsets = tuple( (int(round((new_dim - dim) * offset)) if isinstance(offset, float) else offset) % dim for dim, new_dim, offset in zip(arr.shape, shape, offsets)) assert (arr.ndim == len(shape) == len(offsets)) views = tuple( tuple( slice(max(0, dim * i - offset), dim * (i + 1) - offset) for i in range((new_dim + offset) // dim)) + (slice(dim * ((new_dim + offset) // dim) - offset, new_dim), ) for offset, dim, new_dim in zip(offsets, arr.shape, shape)) views = tuple( tuple(slice_ for slice_ in view if slice_.start < slice_.stop) for view in views) result = np.zeros(shape, dtype=arr.dtype) for view in itertools.product(*views): slicing = tuple( slice(None) if slice_.stop - slice_.start == dim else ( slice(offset, offset + (slice_.stop - slice_.start)) if slice_.start == 0 else slice(0, (slice_.stop - slice_.start))) for slice_, offset, dim in zip(view, offsets, arr.shape)) result[view] = arr[slicing] return result
def reframe(traj, bounds=(-1, 1)): """ Scale the coordinates of the trajectory to be within the specified bounds. Args: traj (np.ndarray): The coordinates of the trajectory. The shape is: (n_dim, n_points). bounds (Iterable[int|float|Iterable[int|float]: The scaling bounds. If Iterable of int or float, must have size 2, corresponding to the min and max bounds for all dimensions. If Iterable of Iterable, the outer Iterable must match the dimensions of the trajectory, while the inner Iterables must have size 2, corresponding to the min and max bounds for each dimensions. Returns: traj (np.ndarray): The coordinates of the trajectory. This is scaled to fit in the specified bounds. Examples: >>> traj, mask = zig_zag_blipped_2d(5, 1, 2) >>> print(traj) [[0 1 2 3 4 4 3 2 1 0] [0 0 0 0 0 1 1 1 1 1]] >>> print(reframe(traj, (-1, 1))) [[-1. -0.5 0. 0.5 1. 1. 0.5 0. -0.5 -1. ] [-1. -1. -1. -1. -1. 1. 1. 1. 1. 1. ]] >>> print(reframe(traj, ((0, 8), (0, 3)))) [[0. 2. 4. 6. 8. 8. 6. 4. 2. 0.] [0. 0. 0. 0. 0. 3. 3. 3. 3. 3.]] >>> print(reframe(traj, (2, 3, 1))) Traceback (most recent call last): ... ValueError: Invalid `bounds` format. >>> print(reframe(traj, ((0, 1, 8), (0, 3)))) Traceback (most recent call last): ... ValueError: Invalid `bounds` format. """ n_dims = traj.shape[0] try: [len(x) for x in bounds] except (IndexError, TypeError): bounds = fc.auto_repeat(bounds, n_dims, True, True) if any(len(x) != 2 for x in bounds): text = 'Invalid `bounds` format.' raise ValueError(text) traj = traj.astype(float) for i in range(n_dims): traj[i] = fcn.scale(traj[i], bounds[i]) return traj
def stacked_circular_loops( radiuses, positions, currents, position=0.5, normal=(0., 1., 0.), n_loops=None): """ Generate parallel circular loops. Args: radiuses (int|float|Iterable[int|float]): The radiusies of the loops. positions (int|float|Iterable[int|float]): The positions of the loops. These are the positions of the loops relative to `position` and along the `normal` direction. currents (int|float|Iterable[int|float]): The currents in the loops. position (float|Iterable[float]): The position of the center. Values are relative to the lowest edge. normal (Iterable[int|float]): The orientation (normal) of the loop. This is a 2D or 3D unit vector. The any magnitude information will be lost. n_loops (int|None): The number of loops. If None, this is inferred from the other parameters, but at least one of `radiuses`, `currents` must be iterable. Returns: circ_loops (list[CircularLoop]): The circular loops. """ if n_loops is None: n_loops = fc.combine_iter_len(radiuses, positions, currents) radiuses = fc.auto_repeat(radiuses, n_loops, check=True) positions = fc.auto_repeat(positions, n_loops, check=True) currents = fc.auto_repeat(currents, n_loops, check=True) # : compute circular loop centers n_dim = 3 position = np.array(fc.auto_repeat(position, n_dim, check=True)) normal = np.array(fcn.normalize(normal)) centers = [position + x * normal for x in positions] circ_loops = [ CircularLoop(radius, center, normal, current) for center, radius, current in zip(centers, radiuses, currents)] return circ_loops
def threshold_relative(arr, values=0.5): """ Calculate threshold relative to array values range. Args: arr (np.ndarray): The input array. values (float|Iterable[float]): The relative threshold value(s). Values must be in the [0, 1] range. Returns: result (tuple[float]): the calculated threshold. """ min_val = np.min(arr) max_val = np.max(arr) values = fc.auto_repeat(values, 1) return tuple(min_val + (max_val - min_val) * float(value) for value in values)
def threshold_percentile(arr, values=0.5): """ Calculate threshold percentile. This is the threshold value at which X% (= value) of the data is smaller than the threshold. Args: arr (np.ndarray): The input array. values (float|Iterable[float]): The percentile value(s). Values must be in the [0, 1] range. Returns: result (tuple[float]): the calculated thresholds. """ values = fc.auto_repeat(values, 1) values = tuple(100.0 * value for value in values) return tuple(np.percentile(arr, values))
def zoom(traj, factors=1): """ Scale the coordinates of the trajectory by the specified factors. Args: traj (np.ndarray): The coordinates of the trajectory. The shape is: (n_dim, n_points). factors (int|float|Iterable[int|float]): The scaling factor(s). If int or float, the same factor is used for all dimensions. If Iterable, must match the dimensions of the trajectory. Returns: traj (np.ndarray): The coordinates of the trajectory. The shape is: (n_dim, n_points). The values are scaled according to the specified factors. Examples: >>> traj, mask = zig_zag_blipped_2d(5, 1, 2) >>> print(traj) [[0 1 2 3 4 4 3 2 1 0] [0 0 0 0 0 1 1 1 1 1]] >>> print(zoom(traj, 2)) [[0 2 4 6 8 8 6 4 2 0] [0 0 0 0 0 2 2 2 2 2]] >>> print(zoom(traj, (2, 3))) [[0 2 4 6 8 8 6 4 2 0] [0 0 0 0 0 3 3 3 3 3]] >>> print(zoom(traj, (2, 3, 1))) Traceback (most recent call last): ... AssertionError """ n_dims = traj.shape[0] factors = fc.auto_repeat(factors, n_dims, False, True) factors = np.array(factors).reshape(-1, 1) traj = traj * factors return traj
def qsm_preprocess(mag_arr, phs_arr, echo_times, echo_times_mask=None): """ EXPERIMENTAL! Args: mag_arr (): phs_arr (): echo_times (): echo_times_mask (): Returns: """ echo_times = np.array(fc.auto_repeat(echo_times, 1)) if len(echo_times) > 1: dphs_arr = phs.phs_to_dphs(phs_arr, tis=echo_times, tis_mask=echo_times_mask) mag_arr = mag_arr[..., 0] else: dphs_arr = phs.phs_to_dphs(phs_arr, echo_times[0]) mask_arr = mrt.segmentation.mask_threshold_compact(mag_arr) raise NotImplementedError
def sphere_with_circular_loops( position=0.5, angles=0.0, diameter=0.8, n_loops=24, radiuses=None, currents=1, coord_gen=lambda x: fcn.fibonacci_sphere(x).transpose()): """ Generate circular loops along the surface of a sphere. Args: position (float|Iterable[float]): The position of the center. Values are relative to the lowest edge. angles (int|float|Iterable[int|float]): The angles of rotation in deg. These are used to rotate the positions of the circular loops in the sphere surface. Each angle specifies the rotation using the canonical basis as axes of rotation. If float, the value is repeated for all axes. Otherwise, it must have size 3 (for more info on the number of angles in 3 dimensions, see `fcn.square_size_to_num_tria()`). The rotation is computed using `fcn.angles2linear(angles)`. See that for more info. diameter (int|float): The diameter of the sphere. n_loops (int|None): The total number of loops. radiuses (int|float|Iterable[int|float]|None): The loop radiuses. If int or float, the same value is used for all loops. If Iterable, its size must be `n_loops`. If None, the radius is the same for all loops and is computed to be 1/2 of the minimum distance between any two centers. This ensures that the loops do not overlap. currents (int|float|Iterable[int|float]): The currents in the loops. coord_gen (callable): The generator for loop centers. This is a function that computes the cartesian coordinates of points on the surface of a unitary sphere. Must have the following signature: coord_gen(int) -> Iterable. Each element of the iterable must have size 3. Returns: circ_loops (list[CircularLoop]): The circular loops. """ n_dim = 3 position = np.array(fc.auto_repeat(position, n_dim, check=True)) currents = fc.auto_repeat(currents, n_loops, check=True) angles = fc.auto_repeat( angles, fcn.square_size_to_num_tria(n_dim), check=True) rot_matrix = fcn.angles2rotation(angles) centers = [ np.dot(rot_matrix, center * diameter / 2) + position for center in coord_gen(n_loops)] normals = [ fcn.vectors2direction(center, position) for center in centers] if not radiuses: radiuses = min(fcn.pairwise_distances(centers)) / 2 radiuses = fc.auto_repeat(radiuses, n_loops, check=True) circ_loops = [ CircularLoop(radius, center, normal, current) for radius, center, normal, current in zip(radiuses, centers, normals, currents)] return circ_loops
def cylinder_with_circular_loops( position=0.5, diameters=0.6, angle=0.0, angle_offset=0.0, height=0.8, direction=(0., 0., 1.), radiuses=None, distance_factors=1, currents=1, n_series=6, loops_per_series=4, n_loops=None): """ Generate circular loops along the lateral surface of a cylinder. The cylinder may have circular or elliptical basis. Args: position (float|Iterable[float]): The position of the center. Values are relative to the lowest edge. diameters (int|float|Iterable[int|float]): The diameter(s) of the base. If int or float, this is the diameter of the base of the circular cylinder. If Iterable, size must be 2, and these are the the major and minor axes (i.e. the diameters) of the elliptical cylinder base. angle (int|float): The rotation of the cylinder basis in deg. angle_offset (int|float): The phase offset of the angle in deg. height (int|float): The height of the cylinder. direction (Iterable[int|float]: The direction of the cylinder. Must have size 3. radiuses (int|float|Iterable[int|float]|None): The loop radiuses. If int or float, the same value is used for all loops. If Iterable, its size must be `n_loops`. If None, the radius is the same for all loops and is computed so that the loops in each series cover the all cylinder height without overlapping. The overlapping behavior can be tweaked using `distance_factors` smaller than 1. distance_factors (int|float|Iterable[int|float]): The distance factors. These determine the distance of the circular loops within each series. currents (int|float|Iterable[int|float]): The currents in the loops. n_series (int|None): The number of loop series. The series are equally distributed along the lateral surface of the cylinder. If None, this is computed from `n_loops` and `loops_per_series`, and they cannot be None. loops_per_series (int|None): The number of loop per series. If None, this is computed from `n_loops` and `n_series`, and they cannot be None. n_loops (int|None): The total number of loops. If None, this is computed from `n_series` and `loops_per_series`, and they cannot be None. Returns: circ_loops (list[CircularLoop]): The circular loops. """ n_dim = 3 if not n_loops and n_series and loops_per_series: n_loops = n_series * loops_per_series elif n_loops and not n_series and loops_per_series: n_series = n_loops // loops_per_series if n_loops % loops_per_series: text = 'Values of `n_loops={}` and `loops_per_serie={}` ' \ 'do not match.'.format(n_loops, loops_per_series) raise ValueError(text) elif n_loops and n_series and not loops_per_series: loops_per_series = n_loops // n_series if n_loops % n_series: text = 'Values of `n_loops={}` and `n_series={}` ' \ 'do not match.'.format(n_loops, n_series) raise ValueError(text) else: text = 'At least two of `n_loops`, `n_series` and `loops_per_serie` ' \ 'must be larger than 0' raise ValueError(text) position = np.array(fc.auto_repeat(position, n_dim, check=True)) diameters = fc.auto_repeat(diameters, 2, check=True) currents = fc.auto_repeat(currents, n_loops, check=True) if not radiuses: radiuses = height / loops_per_series / 2 radiuses = fc.auto_repeat(radiuses, n_loops, check=True) distance_factors = fc.auto_repeat( distance_factors, loops_per_series - 1) distances = tuple( k * (r_m1 + r_p1) for k, r_m1, r_p1 in zip(distance_factors, radiuses[:-1], radiuses[1:])) orientation = np.array((0., 0., 1.)) rot_matrix = np.dot( fcn.rotation_3d_from_vector(orientation, angle), fcn.rotation_3d_from_vectors(orientation, direction)) centers, normals = [], [] a, b = [x / 2 for x in diameters] for phi in fcn.angles_in_ellipse( n_series, a, b, np.deg2rad(angle_offset)): for k in fcn.distances2displacements(distances): center = np.array([a * np.cos(phi), b * np.sin(phi), k]) centers.append(np.dot(rot_matrix, center) + position) normal = fcn.normalize( np.array([-a * np.cos(phi), -b * np.sin(phi), 0])) normals.append(np.dot(rot_matrix, normal)) circ_loops = [ CircularLoop(radius, center, normal, current) for radius, center, normal, current in zip(radiuses, centers, normals, currents)] return circ_loops
def reframe(arr, new_shape, position=0.5, background=0.0): """ Add a frame to an array by centering the input array into a new shape. Args: arr (np.ndarray): The input array. new_shape (int|Iterable[int]): The shape of the output array. If int, uses the same value for all dimensions. If Iterable, the size must match `arr` dimensions. Additionally, each value of `new_shape` must be greater than or equal to the corresponding dimensions of `arr`. position (int|float|Iterable[int|float]): Position within new shape. Determines the position of the array within the new shape. If int or float, it is considered the same in all dimensions, otherwise its length must match the number of dimensions of the array. If int or Iterable of int, the values are absolute and must be less than or equal to the difference between the shape of the array and the new shape. If float or Iterable of float, the values are relative and must be in the [0, 1] range. background (int|float): The background value to be used for the frame. Returns: result (np.ndarray): The result array with added borders. Raises: IndexError: input and output shape sizes must match. ValueError: output shape cannot be smaller than the input shape. See Also: - flyingcircus_numeric.frame() - flyingcircus_numeric.padding() Examples: >>> arr = np.ones((2, 3)) >>> reframe(arr, (4, 5)) array([[0., 0., 0., 0., 0.], [0., 1., 1., 1., 0.], [0., 1., 1., 1., 0.], [0., 0., 0., 0., 0.]]) >>> reframe(arr, (4, 5), 0) array([[1., 1., 1., 0., 0.], [1., 1., 1., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.]]) >>> reframe(arr, (4, 5), (2, 0)) array([[0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [1., 1., 1., 0., 0.], [1., 1., 1., 0., 0.]]) >>> reframe(arr, (4, 5), (0.0, 1.0)) array([[0., 0., 1., 1., 1.], [0., 0., 1., 1., 1.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.]]) """ new_shape = fc.auto_repeat(new_shape, arr.ndim, check=True) position = fc.auto_repeat(position, arr.ndim, check=True) if any(old > new for old, new in zip(arr.shape, new_shape)): raise ValueError('new shape cannot be smaller than the old one.') position = tuple( int(round((new - old) * x_i)) if isinstance(x_i, float) else x_i for old, new, x_i in zip(arr.shape, new_shape, position)) if any(old + x_i > new for old, new, x_i in zip(arr.shape, new_shape, position)): raise ValueError( 'Incompatible `new_shape`, `array shape` and `position`.') result = np.full(new_shape, background) inner = tuple( slice(offset, offset + dim, None) for dim, offset in zip(arr.shape, position)) result[inner] = arr return result
def auto_thresholds(arr, method='otsu', kws=None): """ Calculate a thresholding value based on the specified method. Args: arr (np.ndarray): The input array. method (str): The threshold method. Accepted values are: - 'relative': use `pymrt.segmentation.threshold_relative()`. - 'percentile': use `pymrt.segmentation.threshold_percentile()`. - 'mean_std': use `pymrt.segmentation.threshold_mean_std()`. - 'otsu': use `pymrt.segmentation.threshold_otsu()`. - 'otsu2': use `pymrt.segmentation.threshold_otsu2()`. - 'hist_peaks': use `pymrt.segmentation.threshold_hist_peaks()`. - 'inv_hist_peaks': use `pymrt.segmentation.threshold_inv_hist_peaks()`. - 'hist_peak_edges': use `pymrt.segmentation.threshold_hist_peak_edges()`. - 'inv_hist_peak_edges': use `pymrt.segmentation.threshold_inv_hist_peak_edges()`. - 'twice_first_peak': use `pymrt.segmentation.threshold_twice_first_peak()`. - 'cum_hist_elbow': use `pymrt.segmentation.threshold_cum_hist_elbow()`. - 'rayleigh': use `pymrt.segmentation.threshold_rayleigh()`. - 'optim': use `pymrt.segmentation.threshold_optim()`. kws (dict|None): Keyword parameters for the selected method. Returns: thresholds (tuple[float]): The threshold(s). Raises: ValueError: If `method` is unknown. """ if method: method = method.lower() methods = ( 'relative', 'percentile', 'mean_std', 'otsu', 'otsu2', 'hist_peaks', 'inv_hist_peaks', 'hist_peak_edges', 'inv_hist_peak_edges', 'twice_first_peak', # 'cum_hist_elbow', 'cum_hist_quad', # 'cum_hist_quad_weight', 'cum_hist_quad_inv_weight', 'rayleigh', 'optim') if kws is None: kws = dict() if method == 'relative': thresholds = threshold_relative(arr, **dict(kws)) elif method == 'percentile': thresholds = threshold_percentile(arr, **dict(kws)) elif method == 'mean_std': thresholds = threshold_mean_std(arr, **dict(kws)) elif method == 'otsu': thresholds = threshold_otsu(arr, **dict(kws)) elif method == 'otsu2': thresholds = threshold_otsu2(arr, **dict(kws)) elif method == 'hist_peaks': thresholds = threshold_hist_peaks(arr, **dict(kws)) elif method == 'inv_hist_peaks': thresholds = threshold_inv_hist_peaks(arr, **dict(kws)) elif method == 'hist_peak_edges': thresholds = threshold_hist_peak_edges(arr, **dict(kws)) elif method == 'inv_hist_peak_edges': thresholds = threshold_inv_hist_peak_edges(arr, **dict(kws)) elif method == 'twice_first_peak': thresholds = threshold_twice_first_peak(arr, **dict(kws)) elif method == 'cum_hist_elbow': thresholds = threshold_cum_hist_elbow(arr, **dict(kws)) elif method == 'rayleigh': thresholds = threshold_rayleigh(arr, **dict(kws)) elif method == 'optim': thresholds = threshold_optim(arr, **dict(kws)) else: # if method not in methods: raise ValueError('valid methods are: {} (given: {})'.format( methods, method)) # ensures that the result is Iterable thresholds = tuple(fc.auto_repeat(thresholds, 1)) return thresholds
def b_field( self, shape, n_dim=3, rel_position=True, rel_sizes=max, zero_cutoff=np.spacing(1.0)): """ Compute the magnetic field generated by the object. For 2D inputs, the normal of the circular loop is assumed to be in the 2D plane and only the field in that plane is computed. Args: shape (int|Iterable[int]): The shape of the container in px. n_dim (int|None): The number of dimensions of the input. If None, the number of dims is guessed from the other parameters, but `shape` must be iterable. rel_position (bool|callable): Use positions as relative values. Determine the interpretation of `center` using `shape`. Uses `fcn.grid_coord()` internally, see its `is_relative` parameter for more details. rel_sizes (bool|callable): Use sizes as relative values. Determine the interpretation of `radius` using `shape`. Uses `fcn.coord()` internally, see its `is_relative` parameter for more details. zero_cutoff (float|None): The threshold for masking zero values. If None, no cut-off is performed. Returns: b_arr (np.ndarray): The B 3D-vector field. The first dim contains the cartesian components of the field: B_x = b_arr[0, ...], B_y = b_arr[1, ...], etc. Even if the input is 2D, the result is always a 3D vector field. The 3D vector field is represented as a 4D array (with the 1st dim of size 3). References: - Bergeman, T., Gidon Erez, and Harold J. Metcalf. “Magnetostatic Trapping Fields for Neutral Atoms.” Physical Review A 35, no. 4 (February 1, 1987): 1535–46. https://doi.org/10.1103/PhysRevA.35.1535. - Simpson, James C. Lane. “Simple Analytic Expressions for the Magnetic Field of a Circular Current Loop,” January 1, 2001. https://ntrs.nasa.gov/search.jsp?R=20010038494. """ # : extend 2D to 3D if n_dim is None: n_dim = fc.combine_iter_len((shape,)) if n_dim == 2: shape = fc.auto_repeat(shape, n_dim, check=True) + (1,) if np.isclose(np.linalg.norm(self.normal), 0.0): self.normal = np.array([0., 0., 1.]) elif n_dim == 3: shape = fc.auto_repeat(shape, n_dim, check=True) else: raise ValueError('The number of dimensions must be either 2 or 3.') # : generate coordinates normal = np.array([0., 0., 1.]) # : rotate coordinates ([0, 0, 1] is the standard loop normal) xx = fcn.grid_coord( shape, self.center, is_relative=rel_position, use_int=False) rot_matrix = fcn.rotation_3d_from_vectors(normal, self.normal) irot_matrix = fcn.rotation_3d_from_vectors(self.normal, normal) if not np.all(normal == self.normal): xx = fcn.grid_transform(xx, rot_matrix) # : remove zeros if zero_cutoff is not None: for i in range(n_dim): xx[i][np.abs(xx[i]) < zero_cutoff] = zero_cutoff # inline `rr2` for lower memory footprint (but running will be slower) rr2 = (xx[0] ** 2 + xx[1] ** 2 + xx[2] ** 2) aa = fcn.coord( shape, self.radius, is_relative=rel_sizes, use_int=False)[0] cc = self.current * sp.constants.mu_0 / np.pi rho2 = (xx[0] ** 2 + xx[1] ** 2) ah2 = aa ** 2 + rr2 - 2 * aa * np.sqrt(rho2) bh2 = aa ** 2 + rr2 + 2 * aa * np.sqrt(rho2) ekk2 = sp.special.ellipe(1 - ah2 / bh2) kkk2 = sp.special.ellipkm1(ah2 / bh2) # gh = xx[0] ** 2 - xx[1] ** 2 # not used for the field formulae with np.errstate(divide='ignore', invalid='ignore'): b_x = xx[0] * xx[2] / (2 * ah2 * np.sqrt(bh2) * rho2) * ( (aa ** 2 + rr2) * ekk2 - ah2 * kkk2) b_y = xx[1] * xx[2] / (2 * ah2 * np.sqrt(bh2) * rho2) * ( (aa ** 2 + rr2) * ekk2 - ah2 * kkk2) b_z = 1 / (2 * ah2 * np.sqrt(bh2)) * ( (aa ** 2 - rr2) * ekk2 + ah2 * kkk2) # : clean up some memory del xx, rho2, ah2, bh2, ekk2, kkk2 b_arr = np.stack((b_x, b_y, b_z), 0) del b_x, b_y, b_z # : handle singularities for masker, setter in zip( (np.isnan, np.isposinf, np.isneginf), (np.max, np.max, np.min)): mask = masker(b_arr) b_arr[mask] = setter(b_arr[~mask]) del mask if not np.all(normal == self.normal): b_arr = fcn.grid_transform(b_arr, irot_matrix) return cc * b_arr
def b_field( self, shape, n_dim=3, rel_position=True, rel_sizes=None, zero_cutoff=np.spacing(1.0)): """ Compute the magnetic field generated by the object. For 2D inputs, if the direction is null vector, the wire is assumed to be normal to the 2D plane, and only the in-plane field is computed. Args: shape (int|Iterable[int]): The shape of the container in px. n_dim (int|None): The number of dimensions of the input. If None, the number of dims is guessed from the other parameters, but `shape` must be iterable. rel_position (bool|callable): Use positions as relative values. Determine the interpretation of `position` using `shape`. Uses `fcn.grid_coord()` internally, see its `is_relative` parameter for more details. rel_sizes (bool|callable): Use sizes as relative values. Determine the interpretation of sizes using `shape`. This is actually not used for infinite wires. Uses `fcn.coord()` internally, see its `is_relative` parameter for more details. zero_cutoff (float|None): The threshold for masking zero values. If None, no cut-off is performed. Returns: b_arr (np.ndarray): The B 3D-vector field. The first dim contains the cartesian components of the field: B_x = b_arr[0, ...], B_y = b_arr[1, ...], etc. Even if the input is 2D, the result is always a 3D vector field. The 3D vector field is represented as a 4D array (with the 1st dim of size 3). References: - https://en.wikipedia.org/wiki/Biot%E2%80%93Savart_law """ # : extend 2D to 3D if n_dim is None: n_dim = fc.combine_iter_len((shape,)) if n_dim == 2: shape = fc.auto_repeat(shape, n_dim, check=True) + (1,) if np.isclose(np.linalg.norm(self.direction), 0.0): self.direction = np.array([0., 0., 1.]) elif n_dim == 3: shape = fc.auto_repeat(shape, n_dim, check=True) else: raise ValueError('The number of dimensions must be either 2 or 3.') # : generate coordinates direction = np.array([0., 0., 1.]) # : rotate coordinates ([0, 0, 1] is the standard wire direction) xx = fcn.grid_coord( shape, self.position, is_relative=rel_position, use_int=False) rot_matrix = fcn.rotation_3d_from_vectors(direction, self.direction) irot_matrix = fcn.rotation_3d_from_vectors( self.direction, direction) if not np.all(direction == self.direction): xx = fcn.grid_transform(xx, rot_matrix) # : remove zeros if zero_cutoff is not None: for i in range(n_dim): xx[i][np.abs(xx[i]) < zero_cutoff] = zero_cutoff cc = self.current * sp.constants.mu_0 / (2.0 * np.pi) rho2 = (xx[0] ** 2 + xx[1] ** 2) with np.errstate(divide='ignore', invalid='ignore'): b_x = - xx[1] / rho2 b_y = + xx[0] / rho2 b_z = np.zeros(shape) # : clean up some memory del xx, rho2 b_arr = np.stack( (np.broadcast_to(b_x, shape), np.broadcast_to(b_y, shape), b_z), 0) del b_x, b_y, b_z # : handle singularities for masker, setter in zip( (np.isnan, np.isposinf, np.isneginf), (np.max, np.max, np.min)): mask = masker(b_arr) b_arr[mask] = setter(b_arr[~mask]) del mask if not np.all(direction == self.direction): b_arr = fcn.grid_transform(b_arr, irot_matrix) return cc * b_arr
def sn_split_signals(arr, method='otsu', *_args, **_kws): """ Separate N signal components according to threshold(s). Args: arr (np.ndarray): The input array. method (Iterable[float]|str|callable): The separation method. If Iterable[float], the specified thresholds value are used. If str, the thresholds are estimated using `pymrt.segmentation.auto_thresholds()` with its `method` parameter set to `method`. Additional accepted values: - 'mean': use the mean value of the signal. - 'midval': use the middle of the values range. - 'median': use the median value of the signal. - 'otsu': use the Otsu threshold. If callable, the signature must be: f(np.ndarray, *_args, **_kws) -> Iterable[float] *_args: Positional arguments for `method()`. **_kws: Keyword arguments for `method()`. Returns: result (tuple[np.ndarray]): The tuple contains: - signal1_arr: The first signal component array. - signal2_arr: The second signal component array. Examples: >>> arr = np.array((0, 0, 1, 1, 1, 1, 0, 0)) >>> sn_split_signals(arr) (array([0, 0, 0, 0]), array([1, 1, 1, 1])) >>> arr = np.arange(10) >>> sn_split_signals(arr, method=(2, 6)) (array([0, 1]), array([2, 3, 4, 5]), array([6, 7, 8, 9])) """ if isinstance(method, str): if method == 'mean': thresholds = np.mean(arr) elif method == 'midval': thresholds = mrt.segmentation.threshold_relative(arr, 0.5) elif method == 'median': thresholds = mrt.segmentation.threshold_percentile(arr, 0.5) else: thresholds = mrt.segmentation.auto_thresholds(arr, method, _kws) elif callable(method): thresholds = method(arr, *_args, **_kws) else: thresholds = tuple(method) thresholds = fc.auto_repeat(thresholds, 1) masks = [] full_mask = np.ones(arr.shape, dtype=bool) last_threshold = np.min(arr) for i, threshold in enumerate(sorted(thresholds)): mask = arr < threshold mask *= arr >= last_threshold masks.append(mask) full_mask -= mask last_threshold = threshold return tuple(arr[mask] for mask in masks) + (arr[full_mask], )