from __future__ import annotations
from typing import Union, Sequence
import numpy as np
import warnings
from macromax.utils import dim
[docs]
class Grid(Sequence):
"""
A class representing an immutable uniformly-spaced plaid Cartesian grid and its Fourier Transform.
Unlike the MutableGrid, objects of this class cannot be changed after creation.
See also :class:`MutableGrid`
"""
[docs]
def __init__(self, shape=None, step=None, *, extent=None, first=None, center=None, last=None, include_last=False,
ndim: int = None,
flat: Union[bool, Sequence, np.ndarray] = False,
origin_at_center: Union[bool, Sequence, np.ndarray] = True,
center_at_index: Union[bool, Sequence, np.ndarray] = True):
"""
Construct an immutable `Grid` object.
Its values are defined by `shape`, `step`, `center`, and the boolean flags `include_last` and`center_at_index`.
If not specified, the values for `shape`, `step`, and `center` center are deduced from the other values,
including `first`, `last`, and `extent`. A larger even shape is assumed in case of ambiguity. If all else fails,
first the step is assumed to be 1, then the center is assumed to be as close as possible to 0, and finally the
shape is assumed to be 1.
Specific invariants:
- ```shape * step == extent == last + step - first``` if `include_last`
- ```shape * step == extent == last - first``` if `not include_last`
- ```center == first + shape // 2 * step``` if `center_at_index`
- ```center == first + (shape - 1) / 2 * step``` if `not center_at_index`
General invariants:
- ```shape * step == extent == last + step * include_last - first```
- ```center == first + (shape // 2 * center_at_index + (shape - 1) / 2 * (1 - center_at_index)) * step```
:param shape: An integer vector array with the shape of the sampling grid.
:param step: A vector array with the spacing of the sampling grid. This defaults to 1 if no two of first,
center, or last are specified.
:param extent: The extent of the sampling grid as shape * step = last - first.
:param first: A vector array with the first element for each dimension.
The first element is the smallest element if step is positive, and the largest when step is negative.
:param center: A vector array with the central value for each dimension. The center value is that at the central
index in the grid, rounded up if center_at_index==True, or the average of the surrounding values when False.
The center value defaults to 0 if neither first nor last is specified.
:param last: A vector array with the last element for each dimension. Note that unless include_last is set to
True for the associated dimension, all but the last element is returned when calling self[axis]!
:param include_last: A boolean vector array indicating whether the returned vectors, self[axis], should include
the last element (True) or the penultimate element (default == False). When the step size is not specified,
it is determined so that the step * (shape - 1) == extent.
:param ndim: A scalar integer indicating the number of dimensions of the sampling space.
:param flat: A boolean vector array indicating whether the returned vectors, self[axis], should be
flattened (True) or returned as an open grid (False)
:param origin_at_center: A boolean vector array indicating whether the origin should be fft-shifted (True)
or be ifftshifted to the front (False) of the returned vectors for self[axis].
:param center_at_index: A boolean vector array indicating whether the center of an even dimension is included in
the grid. When left as False and the shape has an even number of elements in the corresponding dimension,
then the next index is used as the center, (self.shape / 2).astype(int). When set to True and the number of
elements in the corresponding dimension is even, then the center value is not included, only its preceding
and following elements.
"""
# Figure out what dimension is required
if ndim is None:
ndim = 0
if shape is not None:
ndim = np.maximum(ndim, np.array(shape).size)
if step is not None:
ndim = np.maximum(ndim, np.array(step).size)
if extent is not None:
ndim = np.maximum(ndim, np.array(extent).size)
if first is not None:
ndim = np.maximum(ndim, np.array(first).size)
if center is not None:
ndim = np.maximum(ndim, np.array(center).size)
if last is not None:
ndim = np.maximum(ndim, np.array(last).size)
self.__ndim = ndim
def is_vector(value):
return value is not None and not np.isscalar(value)
self.__multidimensional = is_vector(shape) or is_vector(step) or is_vector(extent) or \
is_vector(first) or is_vector(center) or is_vector(last)
# Convert all input arguments to vectors of length ndim
shape, step, extent, first, center, last, flat, origin_at_center, include_last, center_at_index = \
self.__all_to_ndim(shape, step, extent, first, center, last, flat, origin_at_center,
include_last, center_at_index)
if shape is None:
if extent is None:
if step is None:
step = 1
# step is known
if last is None:
if first is None:
if center is None:
center = self._to_ndim(0)
# only center and step are known, assume shape == 1
first = center
# first and step are known
if center is None:
center = (first + 1 / step) % step - step / 2 # Pick the step that is closest to 0
# center, first, and step are known
shape = 2 * (center - first) / step - (1 - center_at_index) # Round up to even shape in case of center_at_index
else: # last and step are known
if first is None:
if center is None:
center = (last + 1 / step) % step - step / 2 # Pick the step that is closest to 0
# center, last, and step are known
shape = 2 * (last - center) / step + include_last - (1 - center_at_index) # Round up to even shape if center_at_index
else: # first, last, and step are known
shape = (last - first) / step + include_last
# shape is known
else: # extent is known
if step is None:
step = extent
# step is known
extent = np.sign(step) * np.abs(extent) # Fix sign of extent if it does not agree with that of step
shape = extent / step
# shape is known
# The shape is known
shape = np.maximum(1, np.ceil(shape).astype(int)) # Make sure that the shape is integer and at least 1
if step is None:
if extent is None:
if last is None:
if first is None: # Only (potentially) center and shape are known, assume step = 1
step = self._to_ndim(1)
else:
# first and shape are known
if center is None: # assume step == 1
step = self._to_ndim(1)
else: # center, first, and shape are known
step = (center - first) / (shape // 2 * center_at_index + (shape - 1) / 2 * (1 - center_at_index))
if (center - first).dtype != step.dtype and np.allclose(step, np.round(step)):
step = step.astype(center.dtype)
# step is known
# step is known
else: # last and shape are known
if first is None:
if center is None: # assume step == 1
step = self._to_ndim(1)
else:
# center is known
step = (last - center) / (shape - include_last - shape // 2 * center_at_index - (shape - 1) / 2 * (1 - center_at_index))
if (last - center).dtype != step.dtype and np.allclose(step, np.round(step)):
step = step.astype(center.dtype)
# step is known
else: # first, last, and shape are known
step = (last - first) / (shape - include_last) if np.all(shape > include_last) else np.ones(1)
if (last - first).dtype != step.dtype and np.allclose(step, np.round(step)):
step = step.astype(first.dtype)
# step is known
else: # extent is known
step = extent / shape
if np.all(step == step.astype(int)):
step = step.astype(int)
# step and shape are known
if center is None:
if first is None:
if last is None:
center = self._to_ndim(0)
else: # last is known
center = last - step * (shape - include_last - shape // 2 * center_at_index - (shape - 1) / 2 * (1 - center_at_index))
if (last - step).dtype != center.dtype and np.allclose(center, np.round(center)):
center = center.astype(step.dtype)
# center is known
else: # first is known
center = first + (shape // 2 * center_at_index + (shape - 1) / 2 * (1 - center_at_index)) * step
if (first + step).dtype != center.dtype and np.allclose(center, np.round(center)):
center = center.astype(step.dtype)
# center, step, and shape are known now
# Some sanity checks
if extent is not None and not np.allclose(extent / step, shape) and np.any(np.maximum(1, np.ceil(extent / step)) != shape):
raise ValueError(f"Extent {extent} and step {step} are not compatible with shape {shape} because extent / step = {extent / step} != {shape} = shape.")
if last is not None and first is not None and np.any(shape * step != last + step * include_last - first):
raise ValueError(f"First={first} and last={last} (include_last={include_last}) do not correspond to a step {step} and shape {shape}.")
if center is not None and first is not None and np.any(center != first + (shape // 2 * center_at_index + (shape - 1) / 2 * (1 - center_at_index)) * step):
raise ValueError(f"First={first} and center={center} do not correspond to a step {step} and shape {shape} (center_at_index={center_at_index}).")
if np.any(shape < 1):
warnings.warn(f'shape = {shape}. All input ranges should have at least one element.')
shape = np.maximum(1, shape)
self._shape = shape
dtype = (step[0] + center[0]).dtype if self.ndim > 0 else float
self._step = step.astype(dtype)
self._center = center.astype(dtype)
self._flat = flat
self._origin_at_center = origin_at_center
self.__center_at_index = center_at_index
[docs]
@staticmethod
def from_ranges(*ranges: Union[int, float, complex, Sequence, np.ndarray]) -> Grid:
"""
Converts one or more ranges of numbers to a single Grid object representation.
The ranges can be specified as separate parameters or as a tuple.
:param ranges: one or more ranges of uniformly spaced numbers.
:return: A Grid object that represents the same ranges.
"""
# Convert slices to range vectors. This won't work with infinite slices
ranges = [(np.arange(rng.start, rng.stop, rng.step) if isinstance(rng, slice) else rng) for rng in ranges]
ranges = [np.array([rng] if np.isscalar(rng) else rng) for rng in ranges] # Treat a scalar as a singleton vector
if any(_.size < 1 for _ in ranges):
raise AttributeError('All input ranges should have at least one element.')
ranges = [(rng.swapaxes(0, axis).reshape(rng.shape[axis], -1)[:, 0] if rng.ndim > 1 else rng)
for axis, rng in zip(range(-len(ranges), 0), ranges)]
# Work out some properties about the shape and the size of each dimension
shape = np.array([rng.size for rng in ranges])
singleton = shape <= 1
odd = np.mod(shape, 2) == 1
# Work our what are the first and last elements, which could be at the center
first = np.array([rng[0] for rng in ranges]) # first when fftshifted, center+ otherwise
before_center = np.array([rng[int((rng.size - 1) / 2)] for rng in ranges]) # last when ifftshifted, center+ otherwise
after_center = np.array([rng[-int(rng.size / 2)] for rng in ranges]) # first when ifftshifted, center- otherwise
last = np.array([rng[-1] for rng in ranges]) # last when fftshifted, center- otherwise
# The last value is included!
# If it is not monotonous, it is ifftshifted
origin_at_center = np.abs(last - first) >= np.abs(before_center - after_center)
# Figure out what is the step size and the center element
extent_m1 = origin_at_center * (last - first) + (1 - origin_at_center) * (before_center - after_center)
step = extent_m1 / (shape - 1 + singleton) # Note that the step can be a complex number
center = origin_at_center * (odd * before_center + (1 - odd) * after_center) + (1 - origin_at_center) * first
return Grid(shape=shape, step=step, center=center, flat=False, origin_at_center=origin_at_center)
#
# Grid and array properties
#
@property
def ndim(self) -> int:
"""The number of dimensions of the space this grid spans."""
return self.__ndim
@property
def shape(self) -> np.array:
"""The number of sample points along each axis of the grid."""
return self._shape.copy()
@property
def step(self) -> np.ndarray:
"""The sample spacing along each axis of the grid."""
return self._step.copy()
@property
def center(self) -> np.ndarray:
"""The central coordinate of the grid."""
return self._center.copy()
@property
def center_at_index(self) -> np.array:
"""
Boolean vector indicating whether the central coordinate is aligned with a grid point when the number
of points is even along the associated axis. This has no effect when the the number of sample points is odd.
"""
return self.__center_at_index.copy()
@property
def flat(self) -> np.array:
"""
Boolean vector indicating whether self[axis] returns flattened (raveled) vectors (True) or not (False).
"""
return self._flat.copy()
@property
def origin_at_center(self) -> np.array:
"""
Boolean vector indicating whether self[axis] returns ranges that are monotonous (True) or
ifftshifted so that the central index is the first element of the sequence (False).
"""
return self._origin_at_center.copy()
#
# Conversion methods
#
@property
def as_flat(self) -> Grid:
"""
:return: A new Grid object where all the ranges are 1d-vectors (flattened or raveled)
"""
shape, step, center, center_at_index, origin_at_center = \
self.shape, self.step, self.center, self.center_at_index, self.origin_at_center
if not self.multidimensional:
shape, step, center, center_at_index, origin_at_center = \
shape[0], step[0], center[0], center_at_index[0], origin_at_center[0]
return Grid(shape=shape, step=step, center=center, center_at_index=center_at_index,
flat=True, origin_at_center=origin_at_center)
@property
def as_non_flat(self) -> Grid:
"""
:return: A new Grid object where all the ranges are 1d-vectors (flattened or raveled)
"""
shape, step, center, center_at_index, origin_at_center = \
self.shape, self.step, self.center, self.center_at_index, self.origin_at_center
if not self.multidimensional:
shape, step, center, center_at_index, origin_at_center = \
shape[0], step[0], center[0], center_at_index[0], origin_at_center[0]
return Grid(shape=shape, step=step, center=center, center_at_index=center_at_index,
flat=False, origin_at_center=origin_at_center)
@property
def as_origin_at_0(self) -> Grid:
"""
:return: A new Grid object where all the ranges are ifftshifted so that the origin as at index 0.
"""
shape, step, center, center_at_index, flat = self.shape, self.step, self.center, self.center_at_index, self.flat
if not self.multidimensional:
shape, step, center, center_at_index, flat = shape[0], step[0], center[0], center_at_index[0], flat[0]
return Grid(shape=shape, step=step, center=center, center_at_index=center_at_index,
flat=flat, origin_at_center=False)
@property
def as_origin_at_center(self) -> Grid:
"""
:return: A new Grid object where all the ranges have the origin at the center index, even when the number of elements is odd.
"""
shape, step, center, center_at_index, flat = self.shape, self.step, self.center, self.center_at_index, self.flat
if not self.multidimensional:
shape, step, center, center_at_index, flat = shape[0], step[0], center[0], center_at_index[0], flat[0]
return Grid(shape=shape, step=step, center=center, center_at_index=center_at_index,
flat=flat, origin_at_center=True)
[docs]
def swapaxes(self, axes: Union[slice, Sequence, np.array]) -> Grid:
"""Reverses the order of the specified axes."""
axes = np.array(axes).flatten()
all_axes = np.arange(self.ndim)
all_axes[axes] = axes[::-1]
return self.transpose(all_axes)
[docs]
def transpose(self, axes: Union[None, slice, Sequence, np.array]=None) -> Grid:
"""Reverses the order of all axes."""
if axes is None:
axes = np.arange(self.ndim-1, -1, -1)
return self.project(axes)
[docs]
def project(self, axes_to_keep: Union[int, slice, Sequence, np.array, None] = None,
axes_to_remove: Union[int, slice, Sequence, np.array, None] = None) -> Grid:
"""
Removes all but the specified axes and reduces the dimensions to the number of specified axes.
:param axes_to_keep: The indices of the axes to keep.
:param axes_to_remove: The indices of the axes to remove. Default: None
:return: A Grid object with ndim == len(axes) and shape == shape[axes].
"""
if axes_to_keep is None:
axes_to_keep = np.arange(self.ndim)
elif isinstance(axes_to_keep, slice):
axes_to_keep = np.arange(self.ndim)[axes_to_keep]
if np.isscalar(axes_to_keep):
axes_to_keep = [axes_to_keep]
axes_to_keep = np.array(axes_to_keep)
if axes_to_remove is None:
axes_to_remove = []
elif isinstance(axes_to_remove, slice):
axes_to_remove = np.arange(self.ndim)[axes_to_remove]
if np.isscalar(axes_to_remove):
axes_to_remove = [axes_to_remove]
# Do some checks
if np.any(axes_to_keep >= self.ndim) or np.any(axes_to_keep < -self.ndim):
raise IndexError(f"Axis range {axes_to_keep} requested from a Grid of dimension {self.ndim}.")
# Make sure that the axes are non-negative
axes_to_keep = [_ % self.ndim for _ in axes_to_keep]
axes_to_remove = [_ % self.ndim for _ in axes_to_remove]
axes_to_keep = np.array([_ for _ in axes_to_keep if _ not in axes_to_remove])
if len(axes_to_keep) > 0:
return Grid(shape=self.shape[axes_to_keep], step=self.step[axes_to_keep], center=self.center[axes_to_keep],
flat=self.flat[axes_to_keep],
origin_at_center=self.origin_at_center[axes_to_keep],
center_at_index=self.center_at_index[axes_to_keep]
)
else:
return Grid([])
#
# Derived properties
#
@property
def first(self) -> np.ndarray:
"""A vector with the first element of each range."""
center_is_not_at_index = ~self.center_at_index & (self.shape % 2 == 0)
result = self._center - self.step * (self.shape // 2)
if np.any(center_is_not_at_index):
half_step = self.step // 2 if np.all(self.step % 2 == 0) else self.step / 2
result = result + center_is_not_at_index * half_step
return result
@property
def extent(self) -> np.ndarray:
""" The spatial extent of the sampling grid."""
return self.shape * self.step
#
# Sequence methods
#
@property
def size(self) -> int:
""" The total number of sampling points as an integer scalar. """
return int(np.prod(self.shape))
@property
def dtype(self):
""" The numeric data type for the coordinates. """
return (self.step[0] + self.center[0]).dtype if self.ndim > 0 else float
#
# Frequency grids
#
@property
def f(self) -> Grid:
""" The equivalent frequency Grid. """
with np.errstate(divide='ignore'):
shape, step, flat = self.shape, 1 / self.extent, self.flat
if not self.multidimensional:
shape, step, flat = shape[0], step[0], flat[0]
return Grid(shape=shape, step=step, flat=flat, origin_at_center=False, center_at_index=True)
@property
def k(self) -> Grid:
""" The equivalent k-space Grid. """
return self.f * (2 * np.pi)
#
# Arithmetic methods
#
[docs]
def __add__(self, term) -> Grid:
""" Add a scalar or vector offset to the Grid coordinates. """
d = self.__dict__
new_center = self.center + np.asarray(term)
if not self.multidimensional:
new_center = new_center[0]
d['center'] = new_center
return Grid(**d)
[docs]
def __mul__(self, factor: Union[int, float, complex, Sequence, np.array]) -> Grid:
"""
Scales all ranges with a factor.
:param factor: A scalar factor for all dimensions, or a vector of factors, one for each dimension.
:return: A new scaled Grid object.
"""
if isinstance(factor, Grid):
raise TypeError("A Grid object can't be multiplied with a Grid object."
+ "Use matmul @ to determine the tensor space.")
d = self.__dict__
factor = np.asarray(factor)
new_step = self.step * factor
new_center = self.center * factor
if not self.multidimensional:
new_step = new_step[0]
new_center = new_center[0]
d['step'] = new_step
d['center'] = new_center
return Grid(**d)
[docs]
def __rmul__(self, factor: Union[int, float, complex, Sequence, np.array]) -> Grid:
"""
Scales all ranges with a factor.
:param factor: A scalar factor for all dimensions, or a vector of factors, one for each dimension.
:return: A new scaled Grid object.
"""
return self * factor # Scalars commute.
[docs]
def __matmul__(self, other: Grid) -> Grid:
"""
Determines the Grid spanning the tensor space, with ndim equal to the sum of both ndims.
:param other: The Grid with the right-hand dimensions.
:return: A new Grid with ndim == self.ndim + other.ndim.
"""
return Grid(shape=(*self.shape, *other.shape), step=(*self.step, *other.step),
center=(*self.center, *other.center),
flat=(*self.flat, *other.flat),
origin_at_center=(*self.origin_at_center, *other.origin_at_center),
center_at_index=(*self.center_at_index, *other.center_at_index)
)
[docs]
def __sub__(self, term: Union[int, float, complex, Sequence, np.ndarray]) -> Grid:
""" Subtract a (scalar) value from all Grid coordinates. """
return self + (- term)
[docs]
def __truediv__(self, denominator: Union[int, float, complex, Sequence, np.ndarray]) -> Grid:
"""
Divide the grid coordinates by a value.
:param denominator: The denominator to divide by.
:return: A new Grid with the divided coordinates.
"""
return self * (1 / denominator)
[docs]
def __neg__(self):
""" Invert the coordinate values and the direction of the axes. """
return self.__mul__(-1)
#
# iterator methods
#
[docs]
def __len__(self) -> int:
"""
The number of axes in this sampling grid.
Or, the number of elements when this object is not multi-dimensional.
"""
if self.multidimensional:
return self.ndim
else:
return self.shape[0] # Behave as a single Sequence
def __getitem__(self, key: Union[int, slice, Sequence]):
"""
Select one or more axes from a multi-dimensional grid,
or select elements from a single-dimensional object.
"""
scalar_key = np.isscalar(key)
indices = np.atleast_1d(np.arange(self.ndim if self.multidimensional else self.shape[0])[key])
result = []
for idx in indices.ravel():
axis = idx if self.multidimensional else 0 # Behave as a single Sequence
try:
c, st, sh = self.center[axis], self.step[axis], self.shape[axis]
except IndexError as err:
raise IndexError(f"Axis range {axis} requested from a Grid of dimension {self.ndim}.")
rng = np.arange(sh) - (sh // 2)
if sh > 1: # Define the center as 0 to avoid trouble with * np.inf when this is a singleton dimension.
rng = rng * st
rng = rng + c
if not self.__center_at_index[axis] and (sh % 2 == 0):
rng = rng + st / 2
if not self._origin_at_center[axis]:
rng = np.fft.ifftshift(rng) # Not loading the whole fft library just for this!
if not self.flat[axis]:
rng = dim.to_axis(rng, axis=axis, ndim=self.ndim)
result.append(rng if self.multidimensional else rng[idx])
if scalar_key:
result = result[0] # Unpack again
return result
[docs]
def __iter__(self):
for idx in range(len(self)):
yield self[idx]
#
# General object properties
#
@property
def __dict__(self):
shape, step, center, flat, center_at_index, origin_at_center = \
self.shape, self.step, self.center, self.flat, self.center_at_index, self.origin_at_center
if not self.multidimensional:
shape, step, center, flat, center_at_index, origin_at_center = \
shape[0], step[0], center[0], flat[0], center_at_index[0], origin_at_center[0]
return dict(shape=shape, step=step, center=center, flat=flat,
center_at_index=center_at_index, origin_at_center=origin_at_center)
@property
def immutable(self) -> Grid:
"""Return a new immutable Grid object. """
return Grid(**self.__dict__)
@property
def mutable(self) -> MutableGrid:
"""Return a new MutableGrid object. """
return MutableGrid(**self.__dict__)
def __str__(self) -> str:
core_props = self.__dict__.copy()
arg_desc = ','.join([f'{k}={str(v)}' for k, v in core_props.items()])
return f"{type(self).__name__}({arg_desc:s})"
def __repr__(self) -> str:
core_props = self.__dict__.copy()
core_props['dtype'] = self.dtype
# core_props['multidimensional'] = self.multidimensional
arg_desc = ','.join([f'{k}={repr(v)}' for k, v in core_props.items()])
return f"{type(self).__name__}({arg_desc:s})"
# def __format__(self, format_spec: str = "") -> str:
# return f"{type(self).__name__}({tuple(self.shape)}, ({', '.join(format(_, format_spec) for _ in self.step)}), ({', '.join(format(_, format_spec) for _ in self.center)}))"
#
[docs]
def __eq__(self, other: Grid) -> bool:
""" Compares two Grid objects. """
return type(self) == type(other) and np.all(self.shape == other.shape) and np.all(self.step == other.step) \
and np.all(self.center == other.center) and np.all(self.flat == other.flat) \
and np.all(self.center_at_index == other.center_at_index) \
and np.all(self.origin_at_center == other.origin_at_center) and self.dtype == other.dtype
def __hash__(self) -> int:
return hash(repr(self))
#
# Assorted property
#
@property
def multidimensional(self) -> bool:
"""Single-dimensional grids behave as Sequences, multi-dimensional behave as a Sequence of vectors.
TODO: Remove this feature? It tends to be a source of bugs.
"""
return self.__multidimensional
#
# Protected and private methods
#
def _to_ndim(self, arg) -> np.array:
"""
Helper method to ensure that all arguments are all numpy vectors of the same length, self.ndim.
"""
if arg is not None:
arg = np.array(arg).flatten()
if np.isscalar(arg) or arg.size == 1:
arg = np.repeat(arg, repeats=self.ndim)
elif arg.size != self.ndim:
raise ValueError(
f"All input arguments should be scalar or of length {self.ndim}, not {arg.size} as {arg}.")
return arg
def __all_to_ndim(self, *args):
"""
Helper method to ensures that all arguments are all numpy vectors of the same length, self.ndim.
"""
return tuple([self._to_ndim(arg) for arg in args])
[docs]
class MutableGrid(Grid):
"""
A class representing a mutable uniformly-spaced plaid Cartesian grid and its Fourier Transform.
See also :class:`Grid`
"""
[docs]
def __init__(self, shape=None, step=None, extent=None, first=None, center=None, last=None, include_last=False,
ndim: int = None,
flat: Union[bool, Sequence, np.ndarray] = False,
origin_at_center: Union[bool, Sequence, np.ndarray] = True,
center_at_index: Union[bool, Sequence, np.ndarray] = True):
"""
Construct a mutable Grid object.
:param shape: An integer vector array with the shape of the sampling grid.
:param step: A vector array with the spacing of the sampling grid.
:param extent: The extent of the sampling grid as shape * step
:param first: A vector array with the first element for each dimension.
The first element is the smallest element if step is positive, and the largest when step is negative.
:param center: A vector array with the center element for each dimension. The center position in the grid is
rounded to the next integer index unless center_at_index is set to False for that partical axis.
:param last: A vector array with the last element for each dimension. Unless include_last is set to True for
the associated dimension, all but the last element is returned when calling self[axis].
:param include_last: A boolean vector array indicating whether the returned vectors, self[axis], should include
the last element (True) or all-but-the-last (False)
:param ndim: A scalar integer indicating the number of dimensions of the sampling space.
:param flat: A boolean vector array indicating whether the returned vectors, self[axis], should be
flattened (True) or returned as an open grid (False)
:param origin_at_center: A boolean vector array indicating whether the origin should be fft-shifted (True)
or be ifftshifted to the front (False) of the returned vectors for self[axis].
:param center_at_index: A boolean vector array indicating whether the center of the grid should be rounded to an
integer index for each dimension. If False and the shape has an even number of elements, the next index is
used as the center, (self.shape / 2).astype(int).
"""
super().__init__(shape=shape, step=step, extent=extent, first=first, center=center, last=last,
include_last=include_last, ndim=ndim, flat=flat, origin_at_center=origin_at_center,
center_at_index=center_at_index)
@property
def shape(self) -> np.array:
return super().shape
@shape.setter
def shape(self, new_shape: Union[int, Sequence, np.array]):
if new_shape is not None:
self._shape = self._to_ndim(new_shape)
@property
def step(self) -> np.ndarray:
return super().step
@step.setter
def step(self, new_step: Union[int, float, Sequence, np.array]):
self._step = self._to_ndim(new_step)
self._center = self._center.astype(self.dtype)
@property
def center(self) -> np.ndarray:
return super().center
@center.setter
def center(self, new_center: Union[int, float, Sequence, np.array]):
self._center = self._to_ndim(new_center).astype(self.dtype)
@property
def flat(self) -> np.array:
return super().flat
@flat.setter
def flat(self, value: Union[bool, Sequence, np.array]):
self._flat = self._to_ndim(value)
@property
def origin_at_center(self) -> np.array:
return super().origin_at_center
@origin_at_center.setter
def origin_at_center(self, value: Union[bool, Sequence, np.array]):
self._origin_at_center = self._to_ndim(value)
@property
def first(self) -> np.ndarray:
"""
:return: A vector with the first element of each range
"""
return super().first
@first.setter
def first(self, new_first: Union[int, float, Sequence, np.ndarray]):
self._center = super().center + self._to_ndim(new_first) - self.first
@property
def dtype(self):
""" The numeric data type for the coordinates. """
return (self.step[0] + self.center[0]).dtype
@dtype.setter
def dtype(self, new_type: dtype):
""" Sets the dtype of the range, updating the step and center coordinate."""
self._step = self._step.astype(new_type)
self._center = self._center.astype(new_type)
[docs]
def __iadd__(self, number: Union[int, float, complex, Sequence, np.ndarray]):
self.center += np.asarray(number)
[docs]
def __imul__(self, number: Union[int, float, complex, Sequence, np.ndarray]):
self.step *= np.asarray(number)
self.center *= np.asarray(number)
[docs]
def __isub__(self, number: Union[int, float, complex, Sequence, np.ndarray]):
self.center -= np.asarray(number)
[docs]
def __idiv__(self, number: Union[int, float, complex, Sequence, np.ndarray]):
self.step /= np.asarray(number)
self.center /= np.asarray(number)