Module nlisim.grid
Domain discretization interface.
This module contains defines a common interface for representing the
discretization of the 3D
simulation domain.
In this context, the "grid" is a discrete representation of
the region of 3D space where the simulation occurs (the domain).
The code
will generally assume the domain is the cartesian product of intervals,
\Omega = [x_0, x_1] \times [y_0, y_1] \times [z_0, z_1].
Users should assume that the units of these quantities are in physical quantities
(nanometers) so that, for example,
x_1 - x_0 is the length of the x
-axis
of the domain in nanometers.
There is also no requirement that the lower left
corner of the domain is aligned with the origin.
The grid breaks the continuous domain up into discrete "voxels" each of which
will be centered about a specific point inside the domain.
In general, the
geometry of these voxels is arbitrary and unstructured, but currently only
rectangular grids are
implemented.
For these grids, all voxels are hyper-rectangles and are aligned
along the domains axes.
See the simulation.grid.RectangularGrid
implementation
for details.
Expand source code
r"""
Domain discretization interface.
This module contains defines a common interface for representing the
[discretization](https://en.wikipedia.org/wiki/Discretization) of the 3D
simulation domain. In this context, the "grid" is a discrete representation of
the region of 3D space where the simulation occurs (the domain). The code
will generally assume the domain is the cartesian product of intervals,
\[
\Omega = [x_0, x_1] \times [y_0, y_1] \times [z_0, z_1].
\]
Users should assume that the units of these quantities are in physical quantities
(nanometers) so that, for example, \( x_1 - x_0 \) is the length of the `x`-axis
of the domain in nanometers. There is also no requirement that the lower left
corner of the domain is aligned with the origin.
The grid breaks the continuous domain up into discrete "voxels" each of which
will be centered about a specific point inside the domain. In general, the
geometry of these voxels is arbitrary and unstructured, but currently only
[rectangular grids](https://en.wikipedia.org/wiki/Regular_grid) are
implemented. For these grids, all voxels are hyper-rectangles and are aligned
along the domains axes. See the `simulation.grid.RectangularGrid` implementation
for details.
"""
from functools import reduce
from itertools import product
from typing import Iterable, Iterator, List, Tuple, cast
import attr
from h5py import File as H5File
import numpy as np
from nlisim.coordinates import Point, Voxel
ShapeType = Tuple[int, int, int]
SpacingType = Tuple[float, float, float]
_dtype_float64 = np.dtype('float64')
@attr.s(auto_attribs=True, repr=False)
class RectangularGrid(object):
r"""
A class representation of a rectangular grid.
This class breaks the simulation domain into a \(n_x \times n_y \times
n_z\) array of hyper-rectangles. As is the case for the full domain, each
grid element is cartesian product of intervals,
\[
\Omega_{i,j,k} = [x_i, x_{i+1}] \times [y_j, y_{j+1}] \times [z_k, z_{k+1}].
\]
In addition, there is a "center" for each grid cell contained within the
grid element,
\[
(\bar{x}_i, \bar{y}_j, \bar{z}_k) \in \Omega_{i,j,k}.
\]
For a particular function defined over the domain, \(f\), the
discretization is defined relative to this point,
\[
f_{i,j,k} := f(\bar{x}_i, \bar{y}_j, \bar{z}_k)
\]
This center is usually (but not required to be) the true center of the
interval.
As a concrete example, if you are representing a "gridded variable"
such as the iron concentration throughout the domain as a numpy array, `iron`,
then the value of `iron[k, j, i]` would be the iron concentration at the point
\((\bar{x}_i, \bar{y}_j, \bar{z}_k)\).
This class is initialized with several arrays of numbers representing these
coordinates. Considering just the `x`-axis, the argument `x` is an array
of length \(n_x\). The element of this array at index `i` represents the
value \(\bar{x}_i\). In other words, this array contains coordinate of the
center of all cells in the `x` direction.
The `xv` argument contains the coordinates of the edges of each cell. This
array should be of length \(n_x + 1\) or one element larger than `x`. The
element at index `i` in this array represents the value \(x_i\) or the left
edge of the element. Because the elements are all aligned, this is also the
right edge of the previous element.
Parameters
----------
x : np.ndarray
The `x`-coordinates of the centers of each grid cell.
y : np.ndarray
The `y`-coordinates of the centers of each grid cell.
z : np.ndarray
The `z`-coordinates of the centers of each grid cell.
xv : np.ndarray
The `x`-coordinates of the edges of each grid cell.
yv : np.ndarray
The `y`-coordinates of the edges of each grid cell.
zv : np.ndarray
The `z`-coordinates of the edges of each grid cell.
"""
# cell centered coordinates (1-d arrays)
x: np.ndarray
y: np.ndarray
z: np.ndarray
# vertex coordinates (1-d arrays)
xv: np.ndarray
yv: np.ndarray
zv: np.ndarray
@classmethod
def _make_coordinate_arrays(cls, size: int, spacing: float) -> Tuple[np.ndarray, np.ndarray]:
vertex = np.arange(size + 1) * spacing
cell = spacing / 2 + vertex[:-1]
vertex.flags['WRITEABLE'] = False
cell.flags['WRITEABLE'] = False
return cell, vertex
@classmethod
def construct_uniform(cls, shape: ShapeType, spacing: SpacingType) -> 'RectangularGrid':
"""Create a rectangular grid with uniform spacing in each axis."""
nz, ny, nx = shape
dz, dy, dx = spacing
x, xv = cls._make_coordinate_arrays(nx, dx)
y, yv = cls._make_coordinate_arrays(ny, dy)
z, zv = cls._make_coordinate_arrays(nz, dz)
return cls(x=x, y=y, z=z, xv=xv, yv=yv, zv=zv)
@property
def meshgrid(self) -> List[np.ndarray]:
"""Return the coordinate grid representation.
This returns three 3D arrays containing the z, y, x coordinates
respectively. For example,
>>> Z, Y, X = grid.meshgrid()
`X[zi, yi, xi]` is is the x-coordinate of the point at indices `(xi, yi,
zi)`. The data returned is a read-only view into the coordinate arrays
and is efficient to compute on demand.
"""
return np.meshgrid(self.z, self.y, self.x, indexing='ij', copy=False)
def delta(self, axis: int) -> np.ndarray:
"""Return grid spacing along the given axis."""
if axis == 0:
meshgrid = np.meshgrid(self.zv, self.y, self.x, indexing='ij', copy=False)[axis]
elif axis == 1:
meshgrid = np.meshgrid(self.z, self.yv, self.x, indexing='ij', copy=False)[axis]
elif axis == 2:
meshgrid = np.meshgrid(self.z, self.y, self.xv, indexing='ij', copy=False)[axis]
else:
raise ValueError('Invalid axis provided')
return np.diff(meshgrid, axis=axis)
@property
def shape(self) -> ShapeType:
return (len(self.z), len(self.y), len(self.x))
def __len__(self):
return reduce(lambda x, y: x * y, self.shape, 1)
def allocate_variable(self, dtype: np.dtype = _dtype_float64) -> np.ndarray:
"""Allocate a numpy array defined over this grid."""
return np.zeros(self.shape, dtype=dtype)
def __repr__(self):
shp = self.shape
return f'RectangularGrid(nx={shp[2]}, ny={shp[1]}, nz={shp[0]})'
def save(self, file: H5File) -> None:
"""Save the grid state into an HDF5 file."""
for dim in ('x', 'xv', 'y', 'yv', 'z', 'zv'):
d = file.create_dataset(dim, data=getattr(self, dim))
d.make_scale(dim)
@classmethod
def load(cls, file: H5File) -> 'RectangularGrid':
"""Generate a grid object from an existing HDF5 file."""
kwargs = {}
for dim in ('x', 'xv', 'y', 'yv', 'z', 'zv'):
kwargs[dim] = file[dim][:]
return cls(**kwargs)
@classmethod
def _find_dimension_index(cls, vertices: np.ndarray, coordinate: float) -> int:
indices = (vertices >= coordinate).nonzero()[0]
if len(indices) == 0:
return -1
return indices[0] - 1
def get_flattened_index(self, voxel: Voxel):
"""Return the flattened index of a voxel inside the grid.
This is a convenience method that wraps numpy.ravel_multi_index.
"""
return np.ravel_multi_index(cast(Tuple[int, int, int], voxel), self.shape)
def voxel_from_flattened_index(self, index: int) -> 'Voxel':
"""Create a Voxel from flattened index of the grid.
This is a convenience method that wraps numpy.unravel_index.
"""
z, y, x = np.unravel_index(index, self.shape)
return Voxel(x=float(x), y=float(y), z=float(z))
def get_voxel(self, point: Point) -> Voxel:
"""Return the voxel containing the given point.
For points outside of the grid, this method will return invalid
indices. For example, given vertex coordinates `[1.5, 2.7, 6.5]` and point
`-1.5` or `7.1`, this method will return `-1` and `3`, respectively. Call the
the `is_valid_voxel` method to determine if the voxel is valid.
"""
# For some reason, extracting fields from a recordarray results in a
# transposed point object (shape (1,3) rather than (3,)). This code
# ensures the representation is as expected.
point = point.ravel().view(Point)
assert len(point) == 3, 'This method does not handle arrays of points'
ix = self._find_dimension_index(self.xv, point.x)
iy = self._find_dimension_index(self.yv, point.y)
iz = self._find_dimension_index(self.zv, point.z)
return Voxel(x=ix, y=iy, z=iz)
def get_voxel_center(self, voxel: Voxel) -> Point:
"""Get the coordinates of the center point of a voxel."""
return Point(x=self.x[voxel.x], y=self.y[voxel.y], z=self.z[voxel.z])
def is_valid_voxel(self, voxel: Voxel) -> bool:
"""Return whether or not a voxel index is valid."""
v = voxel
return 0 <= v.x < len(self.x) and 0 <= v.y < len(self.y) and 0 <= v.z < len(self.z)
def is_point_in_domain(self, point: Point) -> bool:
"""Return whether or not a point in inside the domain."""
return (
(self.xv[0] <= point.x <= self.xv[-1])
and (self.yv[0] <= point.y <= self.yv[-1])
and (self.zv[0] <= point.z <= self.zv[-1])
)
def get_adjacent_voxels(self, voxel: Voxel, corners: bool = False) -> Iterator[Voxel]:
"""Return an iterator over all neighbors of a given voxel.
Parameters
----------
voxel : simulation.coordinates.Voxel
The target voxel
corners : bool
Include voxels sharing corners and edges in addition to those sharing sides.
"""
dirs: Iterable[Tuple[int, int, int]]
if corners:
dirs = filter(lambda x: x != (0, 0, 0), product([-1, 0, 1], [-1, 0, 1], [-1, 0, 1]))
else:
dirs = [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1)]
for di, dj, dk in dirs:
i = voxel.x + di
j = voxel.y + dj
k = voxel.z + dk
neighbor = Voxel(x=i, y=j, z=k)
if self.is_valid_voxel(neighbor):
yield neighbor
def get_nearest_voxel(self, point: Point) -> Voxel:
"""Return the nearest voxel to a given point.
Parameters
----------
point : simulation.coordinates.Point
The target point
"""
if not self.is_point_in_domain(point):
raise NotImplementedError(
'Getting the closest domain voxel to a point outside the domain is not implemented'
)
return self.get_voxel(point)
def get_voxels_in_range(self, point: Point, distance: float) -> Iterator[Tuple[Voxel, float]]:
"""Return an iterator of voxels within a given distance of a point.
The values returned by the iterator are tuples of `(Voxel, distance)`
pairs. For example,
voxel, distance = next(self.get_voxels_in_range(point, 1))
where `distance` is the distance from `point` to the center of `voxel`.
Note: no guarantee is given to the order over which the voxels are
iterated.
Parameters
----------
point : simulation.coordinates.Point
The center point
distance : float
Return all voxels with centers less than the distance from the center point
"""
# Get a hyper-square containing a superset of what we want. This
# restricts the set of points that we need to explicitly compute.
dp = Point(x=distance, y=distance, z=distance)
z0, y0, x0 = point - dp
z1, y1, x1 = point + dp
x0 = max(x0, self.x[0])
x1 = min(x1, self.x[-1])
y0 = max(y0, self.y[0])
y1 = min(y1, self.y[-1])
z0 = max(z0, self.z[0])
z1 = min(z1, self.z[-1])
# get voxel indices of the lower left and upper right corners
k0, j0, i0 = self.get_voxel(Point(x=x0, y=y0, z=z0))
k1, j1, i1 = self.get_voxel(Point(x=x1, y=y1, z=z1))
# get a distance matrix over all voxels in the candidate range
z, y, x = self.meshgrid
dx = x[k0 : k1 + 1, j0 : j1 + 1, i0 : i1 + 1] - point.x
dy = y[k0 : k1 + 1, j0 : j1 + 1, i0 : i1 + 1] - point.y
dz = z[k0 : k1 + 1, j0 : j1 + 1, i0 : i1 + 1] - point.z
distances = np.sqrt(dx * dx + dy * dy + dz * dz)
# iterate over all voxels and yield those in range
for k in range(distances.shape[0]):
for j in range(distances.shape[1]):
for i in range(distances.shape[2]):
d = distances[k, j, i]
if d <= distance:
yield Voxel(x=(i + i0), y=(j + j0), z=(k + k0)), d
Classes
class RectangularGrid (x: numpy.ndarray, y: numpy.ndarray, z: numpy.ndarray, xv: numpy.ndarray, yv: numpy.ndarray, zv: numpy.ndarray)
-
A class representation of a rectangular grid.
This class breaks the simulation domain into a n_x \times n_y \times n_z array of hyper-rectangles. As is the case for the full domain, each grid element is cartesian product of intervals, \Omega_{i,j,k} = [x_i, x_{i+1}] \times [y_j, y_{j+1}] \times [z_k, z_{k+1}]. In addition, there is a "center" for each grid cell contained within the grid element, (\bar{x}_i, \bar{y}_j, \bar{z}_k) \in \Omega_{i,j,k}. For a particular function defined over the domain, f, the discretization is defined relative to this point, f_{i,j,k} := f(\bar{x}_i, \bar{y}_j, \bar{z}_k) This center is usually (but not required to be) the true center of the interval.
As a concrete example, if you are representing a "gridded variable" such as the iron concentration throughout the domain as a numpy array,
iron
, then the value ofiron[k, j, i]
would be the iron concentration at the point (\bar{x}_i, \bar{y}_j, \bar{z}_k).This class is initialized with several arrays of numbers representing these coordinates. Considering just the
x
-axis, the argumentx
is an array of length n_x. The element of this array at indexi
represents the value \bar{x}_i. In other words, this array contains coordinate of the center of all cells in thex
direction.The
xv
argument contains the coordinates of the edges of each cell. This array should be of length n_x + 1 or one element larger thanx
. The element at indexi
in this array represents the value x_i or the left edge of the element. Because the elements are all aligned, this is also the right edge of the previous element.Parameters
x
:np.ndarray
- The
x
-coordinates of the centers of each grid cell. y
:np.ndarray
- The
y
-coordinates of the centers of each grid cell. z
:np.ndarray
- The
z
-coordinates of the centers of each grid cell. xv
:np.ndarray
- The
x
-coordinates of the edges of each grid cell. yv
:np.ndarray
- The
y
-coordinates of the edges of each grid cell. zv
:np.ndarray
- The
z
-coordinates of the edges of each grid cell.
Method generated by attrs for class RectangularGrid.
Expand source code
class RectangularGrid(object): r""" A class representation of a rectangular grid. This class breaks the simulation domain into a \(n_x \times n_y \times n_z\) array of hyper-rectangles. As is the case for the full domain, each grid element is cartesian product of intervals, \[ \Omega_{i,j,k} = [x_i, x_{i+1}] \times [y_j, y_{j+1}] \times [z_k, z_{k+1}]. \] In addition, there is a "center" for each grid cell contained within the grid element, \[ (\bar{x}_i, \bar{y}_j, \bar{z}_k) \in \Omega_{i,j,k}. \] For a particular function defined over the domain, \(f\), the discretization is defined relative to this point, \[ f_{i,j,k} := f(\bar{x}_i, \bar{y}_j, \bar{z}_k) \] This center is usually (but not required to be) the true center of the interval. As a concrete example, if you are representing a "gridded variable" such as the iron concentration throughout the domain as a numpy array, `iron`, then the value of `iron[k, j, i]` would be the iron concentration at the point \((\bar{x}_i, \bar{y}_j, \bar{z}_k)\). This class is initialized with several arrays of numbers representing these coordinates. Considering just the `x`-axis, the argument `x` is an array of length \(n_x\). The element of this array at index `i` represents the value \(\bar{x}_i\). In other words, this array contains coordinate of the center of all cells in the `x` direction. The `xv` argument contains the coordinates of the edges of each cell. This array should be of length \(n_x + 1\) or one element larger than `x`. The element at index `i` in this array represents the value \(x_i\) or the left edge of the element. Because the elements are all aligned, this is also the right edge of the previous element. Parameters ---------- x : np.ndarray The `x`-coordinates of the centers of each grid cell. y : np.ndarray The `y`-coordinates of the centers of each grid cell. z : np.ndarray The `z`-coordinates of the centers of each grid cell. xv : np.ndarray The `x`-coordinates of the edges of each grid cell. yv : np.ndarray The `y`-coordinates of the edges of each grid cell. zv : np.ndarray The `z`-coordinates of the edges of each grid cell. """ # cell centered coordinates (1-d arrays) x: np.ndarray y: np.ndarray z: np.ndarray # vertex coordinates (1-d arrays) xv: np.ndarray yv: np.ndarray zv: np.ndarray @classmethod def _make_coordinate_arrays(cls, size: int, spacing: float) -> Tuple[np.ndarray, np.ndarray]: vertex = np.arange(size + 1) * spacing cell = spacing / 2 + vertex[:-1] vertex.flags['WRITEABLE'] = False cell.flags['WRITEABLE'] = False return cell, vertex @classmethod def construct_uniform(cls, shape: ShapeType, spacing: SpacingType) -> 'RectangularGrid': """Create a rectangular grid with uniform spacing in each axis.""" nz, ny, nx = shape dz, dy, dx = spacing x, xv = cls._make_coordinate_arrays(nx, dx) y, yv = cls._make_coordinate_arrays(ny, dy) z, zv = cls._make_coordinate_arrays(nz, dz) return cls(x=x, y=y, z=z, xv=xv, yv=yv, zv=zv) @property def meshgrid(self) -> List[np.ndarray]: """Return the coordinate grid representation. This returns three 3D arrays containing the z, y, x coordinates respectively. For example, >>> Z, Y, X = grid.meshgrid() `X[zi, yi, xi]` is is the x-coordinate of the point at indices `(xi, yi, zi)`. The data returned is a read-only view into the coordinate arrays and is efficient to compute on demand. """ return np.meshgrid(self.z, self.y, self.x, indexing='ij', copy=False) def delta(self, axis: int) -> np.ndarray: """Return grid spacing along the given axis.""" if axis == 0: meshgrid = np.meshgrid(self.zv, self.y, self.x, indexing='ij', copy=False)[axis] elif axis == 1: meshgrid = np.meshgrid(self.z, self.yv, self.x, indexing='ij', copy=False)[axis] elif axis == 2: meshgrid = np.meshgrid(self.z, self.y, self.xv, indexing='ij', copy=False)[axis] else: raise ValueError('Invalid axis provided') return np.diff(meshgrid, axis=axis) @property def shape(self) -> ShapeType: return (len(self.z), len(self.y), len(self.x)) def __len__(self): return reduce(lambda x, y: x * y, self.shape, 1) def allocate_variable(self, dtype: np.dtype = _dtype_float64) -> np.ndarray: """Allocate a numpy array defined over this grid.""" return np.zeros(self.shape, dtype=dtype) def __repr__(self): shp = self.shape return f'RectangularGrid(nx={shp[2]}, ny={shp[1]}, nz={shp[0]})' def save(self, file: H5File) -> None: """Save the grid state into an HDF5 file.""" for dim in ('x', 'xv', 'y', 'yv', 'z', 'zv'): d = file.create_dataset(dim, data=getattr(self, dim)) d.make_scale(dim) @classmethod def load(cls, file: H5File) -> 'RectangularGrid': """Generate a grid object from an existing HDF5 file.""" kwargs = {} for dim in ('x', 'xv', 'y', 'yv', 'z', 'zv'): kwargs[dim] = file[dim][:] return cls(**kwargs) @classmethod def _find_dimension_index(cls, vertices: np.ndarray, coordinate: float) -> int: indices = (vertices >= coordinate).nonzero()[0] if len(indices) == 0: return -1 return indices[0] - 1 def get_flattened_index(self, voxel: Voxel): """Return the flattened index of a voxel inside the grid. This is a convenience method that wraps numpy.ravel_multi_index. """ return np.ravel_multi_index(cast(Tuple[int, int, int], voxel), self.shape) def voxel_from_flattened_index(self, index: int) -> 'Voxel': """Create a Voxel from flattened index of the grid. This is a convenience method that wraps numpy.unravel_index. """ z, y, x = np.unravel_index(index, self.shape) return Voxel(x=float(x), y=float(y), z=float(z)) def get_voxel(self, point: Point) -> Voxel: """Return the voxel containing the given point. For points outside of the grid, this method will return invalid indices. For example, given vertex coordinates `[1.5, 2.7, 6.5]` and point `-1.5` or `7.1`, this method will return `-1` and `3`, respectively. Call the the `is_valid_voxel` method to determine if the voxel is valid. """ # For some reason, extracting fields from a recordarray results in a # transposed point object (shape (1,3) rather than (3,)). This code # ensures the representation is as expected. point = point.ravel().view(Point) assert len(point) == 3, 'This method does not handle arrays of points' ix = self._find_dimension_index(self.xv, point.x) iy = self._find_dimension_index(self.yv, point.y) iz = self._find_dimension_index(self.zv, point.z) return Voxel(x=ix, y=iy, z=iz) def get_voxel_center(self, voxel: Voxel) -> Point: """Get the coordinates of the center point of a voxel.""" return Point(x=self.x[voxel.x], y=self.y[voxel.y], z=self.z[voxel.z]) def is_valid_voxel(self, voxel: Voxel) -> bool: """Return whether or not a voxel index is valid.""" v = voxel return 0 <= v.x < len(self.x) and 0 <= v.y < len(self.y) and 0 <= v.z < len(self.z) def is_point_in_domain(self, point: Point) -> bool: """Return whether or not a point in inside the domain.""" return ( (self.xv[0] <= point.x <= self.xv[-1]) and (self.yv[0] <= point.y <= self.yv[-1]) and (self.zv[0] <= point.z <= self.zv[-1]) ) def get_adjacent_voxels(self, voxel: Voxel, corners: bool = False) -> Iterator[Voxel]: """Return an iterator over all neighbors of a given voxel. Parameters ---------- voxel : simulation.coordinates.Voxel The target voxel corners : bool Include voxels sharing corners and edges in addition to those sharing sides. """ dirs: Iterable[Tuple[int, int, int]] if corners: dirs = filter(lambda x: x != (0, 0, 0), product([-1, 0, 1], [-1, 0, 1], [-1, 0, 1])) else: dirs = [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1)] for di, dj, dk in dirs: i = voxel.x + di j = voxel.y + dj k = voxel.z + dk neighbor = Voxel(x=i, y=j, z=k) if self.is_valid_voxel(neighbor): yield neighbor def get_nearest_voxel(self, point: Point) -> Voxel: """Return the nearest voxel to a given point. Parameters ---------- point : simulation.coordinates.Point The target point """ if not self.is_point_in_domain(point): raise NotImplementedError( 'Getting the closest domain voxel to a point outside the domain is not implemented' ) return self.get_voxel(point) def get_voxels_in_range(self, point: Point, distance: float) -> Iterator[Tuple[Voxel, float]]: """Return an iterator of voxels within a given distance of a point. The values returned by the iterator are tuples of `(Voxel, distance)` pairs. For example, voxel, distance = next(self.get_voxels_in_range(point, 1)) where `distance` is the distance from `point` to the center of `voxel`. Note: no guarantee is given to the order over which the voxels are iterated. Parameters ---------- point : simulation.coordinates.Point The center point distance : float Return all voxels with centers less than the distance from the center point """ # Get a hyper-square containing a superset of what we want. This # restricts the set of points that we need to explicitly compute. dp = Point(x=distance, y=distance, z=distance) z0, y0, x0 = point - dp z1, y1, x1 = point + dp x0 = max(x0, self.x[0]) x1 = min(x1, self.x[-1]) y0 = max(y0, self.y[0]) y1 = min(y1, self.y[-1]) z0 = max(z0, self.z[0]) z1 = min(z1, self.z[-1]) # get voxel indices of the lower left and upper right corners k0, j0, i0 = self.get_voxel(Point(x=x0, y=y0, z=z0)) k1, j1, i1 = self.get_voxel(Point(x=x1, y=y1, z=z1)) # get a distance matrix over all voxels in the candidate range z, y, x = self.meshgrid dx = x[k0 : k1 + 1, j0 : j1 + 1, i0 : i1 + 1] - point.x dy = y[k0 : k1 + 1, j0 : j1 + 1, i0 : i1 + 1] - point.y dz = z[k0 : k1 + 1, j0 : j1 + 1, i0 : i1 + 1] - point.z distances = np.sqrt(dx * dx + dy * dy + dz * dz) # iterate over all voxels and yield those in range for k in range(distances.shape[0]): for j in range(distances.shape[1]): for i in range(distances.shape[2]): d = distances[k, j, i] if d <= distance: yield Voxel(x=(i + i0), y=(j + j0), z=(k + k0)), d
Class variables
var x : numpy.ndarray
var xv : numpy.ndarray
var y : numpy.ndarray
var yv : numpy.ndarray
var z : numpy.ndarray
var zv : numpy.ndarray
Static methods
def construct_uniform(shape: Tuple[int, int, int], spacing: Tuple[float, float, float]) ‑> RectangularGrid
-
Create a rectangular grid with uniform spacing in each axis.
Expand source code
@classmethod def construct_uniform(cls, shape: ShapeType, spacing: SpacingType) -> 'RectangularGrid': """Create a rectangular grid with uniform spacing in each axis.""" nz, ny, nx = shape dz, dy, dx = spacing x, xv = cls._make_coordinate_arrays(nx, dx) y, yv = cls._make_coordinate_arrays(ny, dy) z, zv = cls._make_coordinate_arrays(nz, dz) return cls(x=x, y=y, z=z, xv=xv, yv=yv, zv=zv)
def load(file: h5py._hl.files.File) ‑> RectangularGrid
-
Generate a grid object from an existing HDF5 file.
Expand source code
@classmethod def load(cls, file: H5File) -> 'RectangularGrid': """Generate a grid object from an existing HDF5 file.""" kwargs = {} for dim in ('x', 'xv', 'y', 'yv', 'z', 'zv'): kwargs[dim] = file[dim][:] return cls(**kwargs)
Instance variables
var meshgrid : List[numpy.ndarray]
-
Return the coordinate grid representation.
This returns three 3D arrays containing the z, y, x coordinates respectively. For example,
>>> Z, Y, X = grid.meshgrid()
X[zi, yi, xi]
is is the x-coordinate of the point at indices(xi, yi, zi)
. The data returned is a read-only view into the coordinate arrays and is efficient to compute on demand.Expand source code
@property def meshgrid(self) -> List[np.ndarray]: """Return the coordinate grid representation. This returns three 3D arrays containing the z, y, x coordinates respectively. For example, >>> Z, Y, X = grid.meshgrid() `X[zi, yi, xi]` is is the x-coordinate of the point at indices `(xi, yi, zi)`. The data returned is a read-only view into the coordinate arrays and is efficient to compute on demand. """ return np.meshgrid(self.z, self.y, self.x, indexing='ij', copy=False)
var shape : Tuple[int, int, int]
-
Expand source code
@property def shape(self) -> ShapeType: return (len(self.z), len(self.y), len(self.x))
Methods
def allocate_variable(self, dtype: numpy.dtype = dtype('float64')) ‑> numpy.ndarray
-
Allocate a numpy array defined over this grid.
Expand source code
def allocate_variable(self, dtype: np.dtype = _dtype_float64) -> np.ndarray: """Allocate a numpy array defined over this grid.""" return np.zeros(self.shape, dtype=dtype)
def delta(self, axis: int) ‑> numpy.ndarray
-
Return grid spacing along the given axis.
Expand source code
def delta(self, axis: int) -> np.ndarray: """Return grid spacing along the given axis.""" if axis == 0: meshgrid = np.meshgrid(self.zv, self.y, self.x, indexing='ij', copy=False)[axis] elif axis == 1: meshgrid = np.meshgrid(self.z, self.yv, self.x, indexing='ij', copy=False)[axis] elif axis == 2: meshgrid = np.meshgrid(self.z, self.y, self.xv, indexing='ij', copy=False)[axis] else: raise ValueError('Invalid axis provided') return np.diff(meshgrid, axis=axis)
def get_adjacent_voxels(self, voxel: Voxel, corners: bool = False) ‑> Iterator[Voxel]
-
Return an iterator over all neighbors of a given voxel.
Parameters
voxel
:simulation.coordinates.Voxel
- The target voxel
corners
:bool
- Include voxels sharing corners and edges in addition to those sharing sides.
Expand source code
def get_adjacent_voxels(self, voxel: Voxel, corners: bool = False) -> Iterator[Voxel]: """Return an iterator over all neighbors of a given voxel. Parameters ---------- voxel : simulation.coordinates.Voxel The target voxel corners : bool Include voxels sharing corners and edges in addition to those sharing sides. """ dirs: Iterable[Tuple[int, int, int]] if corners: dirs = filter(lambda x: x != (0, 0, 0), product([-1, 0, 1], [-1, 0, 1], [-1, 0, 1])) else: dirs = [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1)] for di, dj, dk in dirs: i = voxel.x + di j = voxel.y + dj k = voxel.z + dk neighbor = Voxel(x=i, y=j, z=k) if self.is_valid_voxel(neighbor): yield neighbor
def get_flattened_index(self, voxel: Voxel)
-
Return the flattened index of a voxel inside the grid.
This is a convenience method that wraps numpy.ravel_multi_index.
Expand source code
def get_flattened_index(self, voxel: Voxel): """Return the flattened index of a voxel inside the grid. This is a convenience method that wraps numpy.ravel_multi_index. """ return np.ravel_multi_index(cast(Tuple[int, int, int], voxel), self.shape)
def get_nearest_voxel(self, point: Point) ‑> Voxel
-
Return the nearest voxel to a given point.
Parameters
point
:simulation.coordinates.Point
- The target point
Expand source code
def get_nearest_voxel(self, point: Point) -> Voxel: """Return the nearest voxel to a given point. Parameters ---------- point : simulation.coordinates.Point The target point """ if not self.is_point_in_domain(point): raise NotImplementedError( 'Getting the closest domain voxel to a point outside the domain is not implemented' ) return self.get_voxel(point)
def get_voxel(self, point: Point) ‑> Voxel
-
Return the voxel containing the given point.
For points outside of the grid, this method will return invalid indices. For example, given vertex coordinates
[1.5, 2.7, 6.5]
and point-1.5
or7.1
, this method will return-1
and3
, respectively. Call the theis_valid_voxel
method to determine if the voxel is valid.Expand source code
def get_voxel(self, point: Point) -> Voxel: """Return the voxel containing the given point. For points outside of the grid, this method will return invalid indices. For example, given vertex coordinates `[1.5, 2.7, 6.5]` and point `-1.5` or `7.1`, this method will return `-1` and `3`, respectively. Call the the `is_valid_voxel` method to determine if the voxel is valid. """ # For some reason, extracting fields from a recordarray results in a # transposed point object (shape (1,3) rather than (3,)). This code # ensures the representation is as expected. point = point.ravel().view(Point) assert len(point) == 3, 'This method does not handle arrays of points' ix = self._find_dimension_index(self.xv, point.x) iy = self._find_dimension_index(self.yv, point.y) iz = self._find_dimension_index(self.zv, point.z) return Voxel(x=ix, y=iy, z=iz)
def get_voxel_center(self, voxel: Voxel) ‑> Point
-
Get the coordinates of the center point of a voxel.
Expand source code
def get_voxel_center(self, voxel: Voxel) -> Point: """Get the coordinates of the center point of a voxel.""" return Point(x=self.x[voxel.x], y=self.y[voxel.y], z=self.z[voxel.z])
def get_voxels_in_range(self, point: Point, distance: float) ‑> Iterator[Tuple[Voxel, float]]
-
Return an iterator of voxels within a given distance of a point.
The values returned by the iterator are tuples of
(Voxel, distance)
pairs. For example,voxel, distance = next(self.get_voxels_in_range(point, 1))
where
distance
is the distance frompoint
to the center ofvoxel
.Note: no guarantee is given to the order over which the voxels are iterated.
Parameters
point
:simulation.coordinates.Point
- The center point
distance
:float
- Return all voxels with centers less than the distance from the center point
Expand source code
def get_voxels_in_range(self, point: Point, distance: float) -> Iterator[Tuple[Voxel, float]]: """Return an iterator of voxels within a given distance of a point. The values returned by the iterator are tuples of `(Voxel, distance)` pairs. For example, voxel, distance = next(self.get_voxels_in_range(point, 1)) where `distance` is the distance from `point` to the center of `voxel`. Note: no guarantee is given to the order over which the voxels are iterated. Parameters ---------- point : simulation.coordinates.Point The center point distance : float Return all voxels with centers less than the distance from the center point """ # Get a hyper-square containing a superset of what we want. This # restricts the set of points that we need to explicitly compute. dp = Point(x=distance, y=distance, z=distance) z0, y0, x0 = point - dp z1, y1, x1 = point + dp x0 = max(x0, self.x[0]) x1 = min(x1, self.x[-1]) y0 = max(y0, self.y[0]) y1 = min(y1, self.y[-1]) z0 = max(z0, self.z[0]) z1 = min(z1, self.z[-1]) # get voxel indices of the lower left and upper right corners k0, j0, i0 = self.get_voxel(Point(x=x0, y=y0, z=z0)) k1, j1, i1 = self.get_voxel(Point(x=x1, y=y1, z=z1)) # get a distance matrix over all voxels in the candidate range z, y, x = self.meshgrid dx = x[k0 : k1 + 1, j0 : j1 + 1, i0 : i1 + 1] - point.x dy = y[k0 : k1 + 1, j0 : j1 + 1, i0 : i1 + 1] - point.y dz = z[k0 : k1 + 1, j0 : j1 + 1, i0 : i1 + 1] - point.z distances = np.sqrt(dx * dx + dy * dy + dz * dz) # iterate over all voxels and yield those in range for k in range(distances.shape[0]): for j in range(distances.shape[1]): for i in range(distances.shape[2]): d = distances[k, j, i] if d <= distance: yield Voxel(x=(i + i0), y=(j + j0), z=(k + k0)), d
def is_point_in_domain(self, point: Point) ‑> bool
-
Return whether or not a point in inside the domain.
Expand source code
def is_point_in_domain(self, point: Point) -> bool: """Return whether or not a point in inside the domain.""" return ( (self.xv[0] <= point.x <= self.xv[-1]) and (self.yv[0] <= point.y <= self.yv[-1]) and (self.zv[0] <= point.z <= self.zv[-1]) )
def is_valid_voxel(self, voxel: Voxel) ‑> bool
-
Return whether or not a voxel index is valid.
Expand source code
def is_valid_voxel(self, voxel: Voxel) -> bool: """Return whether or not a voxel index is valid.""" v = voxel return 0 <= v.x < len(self.x) and 0 <= v.y < len(self.y) and 0 <= v.z < len(self.z)
def save(self, file: h5py._hl.files.File) ‑> None
-
Save the grid state into an HDF5 file.
Expand source code
def save(self, file: H5File) -> None: """Save the grid state into an HDF5 file.""" for dim in ('x', 'xv', 'y', 'yv', 'z', 'zv'): d = file.create_dataset(dim, data=getattr(self, dim)) d.make_scale(dim)
def voxel_from_flattened_index(self, index: int) ‑> Voxel
-
Create a Voxel from flattened index of the grid.
This is a convenience method that wraps numpy.unravel_index.
Expand source code
def voxel_from_flattened_index(self, index: int) -> 'Voxel': """Create a Voxel from flattened index of the grid. This is a convenience method that wraps numpy.unravel_index. """ z, y, x = np.unravel_index(index, self.shape) return Voxel(x=float(x), y=float(y), z=float(z))