Esempio n. 1
0
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)
Esempio n. 2
0
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
Esempio n. 3
0
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)
Esempio n. 4
0
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
Esempio n. 5
0
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
Esempio n. 6
0
File: qmt.py Progetto: norok2/pymrt
    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
Esempio n. 7
0
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
Esempio n. 8
0
    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
Esempio n. 9
0
File: phs.py Progetto: norok2/pymrt
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
Esempio n. 10
0
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
Esempio n. 11
0
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
Esempio n. 12
0
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]
Esempio n. 13
0
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
Esempio n. 14
0
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
Esempio n. 15
0
File: phs.py Progetto: norok2/pymrt
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
Esempio n. 16
0
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
Esempio n. 17
0
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
Esempio n. 18
0
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
Esempio n. 19
0
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
Esempio n. 20
0
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)
Esempio n. 21
0
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))
Esempio n. 22
0
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
Esempio n. 23
0
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
Esempio n. 24
0
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
Esempio n. 25
0
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
Esempio n. 26
0
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
Esempio n. 27
0
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
Esempio n. 28
0
    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
Esempio n. 29
0
    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
Esempio n. 30
0
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], )