Compare commits
4 Commits
73d07bbfe0
...
9ab97e763c
Author | SHA1 | Date | |
---|---|---|---|
9ab97e763c | |||
d44e02e2f7 | |||
3e4e6eead3 | |||
a94c2cae67 |
29
.flake8
Normal file
29
.flake8
Normal file
@ -0,0 +1,29 @@
|
||||
[flake8]
|
||||
ignore =
|
||||
# E501 line too long
|
||||
E501,
|
||||
# W391 newlines at EOF
|
||||
W391,
|
||||
# E241 multiple spaces after comma
|
||||
E241,
|
||||
# E302 expected 2 newlines
|
||||
E302,
|
||||
# W503 line break before binary operator (to be deprecated)
|
||||
W503,
|
||||
# E265 block comment should start with '# '
|
||||
E265,
|
||||
# E123 closing bracket does not match indentation of opening bracket's line
|
||||
E123,
|
||||
# E124 closing bracket does not match visual indentation
|
||||
E124,
|
||||
# E221 multiple spaces before operator
|
||||
E221,
|
||||
# E201 whitespace after '['
|
||||
E201,
|
||||
# E741 ambiguous variable name 'I'
|
||||
E741,
|
||||
|
||||
|
||||
per-file-ignores =
|
||||
# F401 import without use
|
||||
*/__init__.py: F401,
|
@ -14,7 +14,7 @@ the coordinates of the boundary points along each axis).
|
||||
## Installation
|
||||
|
||||
Requirements:
|
||||
* python 3 (written and tested with 3.9)
|
||||
* python >3.11 (written and tested with 3.12)
|
||||
* numpy
|
||||
* [float_raster](https://mpxd.net/code/jan/float_raster)
|
||||
* matplotlib (optional, used for visualization functions)
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
Drawing-related methods for Grid class
|
||||
"""
|
||||
from typing import List, Optional, Union, Sequence, Callable
|
||||
from typing import Union, Sequence, Callable
|
||||
|
||||
import numpy
|
||||
from numpy.typing import NDArray, ArrayLike
|
||||
@ -27,7 +27,7 @@ def draw_polygons(
|
||||
center: ArrayLike,
|
||||
polygons: Sequence[NDArray],
|
||||
thickness: float,
|
||||
foreground: Union[Sequence[foreground_t], foreground_t],
|
||||
foreground: Sequence[foreground_t] | foreground_t,
|
||||
) -> None:
|
||||
"""
|
||||
Draw polygons on an axis-aligned plane.
|
||||
@ -59,7 +59,7 @@ def draw_polygons(
|
||||
for i, polygon in enumerate(polygons):
|
||||
malformed = f'Malformed polygon: ({i})'
|
||||
if polygon.shape[1] not in (2, 3):
|
||||
raise GridError(malformed + 'must be a Nx2 or Nx3 ndarray')
|
||||
raise GridError(malformed + 'must be a Nx2 or Nx3 ndarray')
|
||||
if polygon.shape[1] == 3:
|
||||
polygon = polygon[surface, :]
|
||||
|
||||
@ -70,9 +70,9 @@ def draw_polygons(
|
||||
+ 'xyz'[surface_normal])
|
||||
|
||||
# Broadcast foreground where necessary
|
||||
foregrounds: Union[Sequence[foreground_callable_t], Sequence[float]]
|
||||
foregrounds: Sequence[foreground_callable_t] | Sequence[float]
|
||||
if numpy.size(foreground) == 1: # type: ignore
|
||||
foregrounds = [foreground] * len(cell_data) # type: ignore
|
||||
foregrounds = [foreground] * len(cell_data) # type: ignore
|
||||
elif isinstance(foreground, numpy.ndarray):
|
||||
raise GridError('ndarray not supported for foreground')
|
||||
else:
|
||||
@ -113,7 +113,7 @@ def draw_polygons(
|
||||
foregrounds_i = foregrounds[i]
|
||||
if callable(foregrounds_i):
|
||||
# meshgrid over the (shifted) domain
|
||||
domain = [self.shifted_xyz(i)[k][bdi_min[k]:bdi_max[k]+1] for k in range(3)]
|
||||
domain = [self.shifted_xyz(i)[k][bdi_min[k]:bdi_max[k] + 1] for k in range(3)]
|
||||
(x0, y0, z0) = numpy.meshgrid(*domain, indexing='ij')
|
||||
|
||||
# evaluate on the meshgrid
|
||||
@ -202,7 +202,7 @@ def draw_polygon(
|
||||
center: ArrayLike,
|
||||
polygon: ArrayLike,
|
||||
thickness: float,
|
||||
foreground: Union[Sequence[foreground_t], foreground_t],
|
||||
foreground: Sequence[foreground_t] | foreground_t,
|
||||
) -> None:
|
||||
"""
|
||||
Draw a polygon on an axis-aligned plane.
|
||||
@ -226,7 +226,7 @@ def draw_slab(
|
||||
surface_normal: int,
|
||||
center: ArrayLike,
|
||||
thickness: float,
|
||||
foreground: Union[Sequence[foreground_t], foreground_t],
|
||||
foreground: Sequence[foreground_t] | foreground_t,
|
||||
) -> None:
|
||||
"""
|
||||
Draw an axis-aligned infinite slab.
|
||||
@ -276,7 +276,7 @@ def draw_cuboid(
|
||||
cell_data: NDArray,
|
||||
center: ArrayLike,
|
||||
dimensions: ArrayLike,
|
||||
foreground: Union[Sequence[foreground_t], foreground_t],
|
||||
foreground: Sequence[foreground_t] | foreground_t,
|
||||
) -> None:
|
||||
"""
|
||||
Draw an axis-aligned cuboid
|
||||
@ -305,7 +305,7 @@ def draw_cylinder(
|
||||
radius: float,
|
||||
thickness: float,
|
||||
num_points: int,
|
||||
foreground: Union[Sequence[foreground_t], foreground_t],
|
||||
foreground: Sequence[foreground_t] | foreground_t,
|
||||
) -> None:
|
||||
"""
|
||||
Draw an axis-aligned cylinder. Approximated by a num_points-gon
|
||||
@ -319,7 +319,7 @@ def draw_cylinder(
|
||||
num_points: The circle is approximated by a polygon with `num_points` vertices
|
||||
foreground: Value to draw with ('brush color'). See `draw_polygons()` for details.
|
||||
"""
|
||||
theta = numpy.linspace(0, 2*numpy.pi, num_points, endpoint=False)
|
||||
theta = numpy.linspace(0, 2 * numpy.pi, num_points, endpoint=False)
|
||||
x = radius * numpy.sin(theta)
|
||||
y = radius * numpy.cos(theta)
|
||||
polygon = numpy.hstack((x[:, None], y[:, None]))
|
||||
@ -360,8 +360,8 @@ def draw_extrude_rectangle(
|
||||
surface = numpy.delete(range(3), direction)
|
||||
|
||||
dim = numpy.fabs(numpy.diff(rectangle, axis=0).T)[surface]
|
||||
p = numpy.vstack((numpy.array([-1, -1, 1, 1], dtype=float) * dim[0]/2.0,
|
||||
numpy.array([-1, 1, 1, -1], dtype=float) * dim[1]/2.0)).T
|
||||
p = numpy.vstack((numpy.array([-1, -1, 1, 1], dtype=float) * dim[0] * 0.5,
|
||||
numpy.array([-1, 1, 1, -1], dtype=float) * dim[1] * 0.5)).T
|
||||
thickness = distance
|
||||
|
||||
foreground_func = []
|
||||
@ -371,7 +371,7 @@ def draw_extrude_rectangle(
|
||||
ind = [int(numpy.floor(z)) if i == direction else slice(None) for i in range(3)]
|
||||
|
||||
fpart = z - numpy.floor(z)
|
||||
mult = [1-fpart, fpart][::s] # reverses if s negative
|
||||
mult = [1 - fpart, fpart][::s] # reverses if s negative
|
||||
|
||||
foreground = mult[0] * grid[tuple(ind)]
|
||||
ind[direction] += 1 # type: ignore #(known safe)
|
||||
|
@ -1,8 +1,7 @@
|
||||
from typing import List, Tuple, Callable, Dict, Optional, Union, Sequence, ClassVar, TypeVar
|
||||
from typing import Callable, Sequence, ClassVar, Self
|
||||
|
||||
import numpy
|
||||
from numpy.typing import NDArray, ArrayLike
|
||||
from numpy import diff, floor, ceil, zeros, hstack, newaxis
|
||||
|
||||
import pickle
|
||||
import warnings
|
||||
@ -12,7 +11,6 @@ from . import GridError
|
||||
|
||||
|
||||
foreground_callable_type = Callable[[NDArray, NDArray, NDArray], NDArray]
|
||||
T = TypeVar('T', bound='Grid')
|
||||
|
||||
|
||||
class Grid:
|
||||
@ -49,10 +47,10 @@ class Grid:
|
||||
Because of this, we either assume this 'ghost' cell is the same size as the last
|
||||
real cell, or, if `self.periodic[a]` is set to `True`, the same size as the first cell.
|
||||
"""
|
||||
exyz: List[NDArray]
|
||||
exyz: list[NDArray]
|
||||
"""Cell edges. Monotonically increasing without duplicates."""
|
||||
|
||||
periodic: List[bool]
|
||||
periodic: list[bool]
|
||||
"""For each axis, determines how far the rightmost boundary gets shifted. """
|
||||
|
||||
shifts: NDArray
|
||||
@ -80,7 +78,7 @@ class Grid:
|
||||
from .position import ind2pos, pos2ind
|
||||
|
||||
@property
|
||||
def dxyz(self) -> List[NDArray]:
|
||||
def dxyz(self) -> list[NDArray]:
|
||||
"""
|
||||
Cell sizes for each axis, no shifts applied
|
||||
|
||||
@ -90,7 +88,7 @@ class Grid:
|
||||
return [numpy.diff(ee) for ee in self.exyz]
|
||||
|
||||
@property
|
||||
def xyz(self) -> List[NDArray]:
|
||||
def xyz(self) -> list[NDArray]:
|
||||
"""
|
||||
Cell centers for each axis, no shifts applied
|
||||
|
||||
@ -124,7 +122,7 @@ class Grid:
|
||||
return numpy.hstack((self.num_grids, self.shape))
|
||||
|
||||
@property
|
||||
def dxyz_with_ghost(self) -> List[NDArray]:
|
||||
def dxyz_with_ghost(self) -> list[NDArray]:
|
||||
"""
|
||||
Gives dxyz with an additional 'ghost' cell at the end, whose value depends
|
||||
on whether or not the axis has periodic boundary conditions. See main description
|
||||
@ -153,7 +151,7 @@ class Grid:
|
||||
return numpy.array(centers, dtype=float)
|
||||
|
||||
@property
|
||||
def dxyz_limits(self) -> Tuple[NDArray, NDArray]:
|
||||
def dxyz_limits(self) -> tuple[NDArray, NDArray]:
|
||||
"""
|
||||
Returns the minimum and maximum cell size for each axis, as a tuple of two 3-element
|
||||
ndarrays. No shifts are applied, so these are extreme bounds on these values (as a
|
||||
@ -166,7 +164,7 @@ class Grid:
|
||||
d_max = numpy.array([max(self.dxyz[a]) for a in range(3)], dtype=float)
|
||||
return d_min, d_max
|
||||
|
||||
def shifted_exyz(self, which_shifts: Optional[int]) -> List[NDArray]:
|
||||
def shifted_exyz(self, which_shifts: int | None) -> list[NDArray]:
|
||||
"""
|
||||
Returns edges for which_shifts.
|
||||
|
||||
@ -188,7 +186,7 @@ class Grid:
|
||||
|
||||
return [self.exyz[a] + dxyz[a] * shifts[a] for a in range(3)]
|
||||
|
||||
def shifted_dxyz(self, which_shifts: Optional[int]) -> List[NDArray]:
|
||||
def shifted_dxyz(self, which_shifts: int | None) -> list[NDArray]:
|
||||
"""
|
||||
Returns cell sizes for `which_shifts`.
|
||||
|
||||
@ -215,7 +213,7 @@ class Grid:
|
||||
|
||||
return sdxyz
|
||||
|
||||
def shifted_xyz(self, which_shifts: Optional[int]) -> List[NDArray[numpy.float64]]:
|
||||
def shifted_xyz(self, which_shifts: int | None) -> list[NDArray[numpy.float64]]:
|
||||
"""
|
||||
Returns cell centers for `which_shifts`.
|
||||
|
||||
@ -231,7 +229,7 @@ class Grid:
|
||||
dxyz = self.shifted_dxyz(which_shifts)
|
||||
return [exyz[a][:-1] + dxyz[a] / 2.0 for a in range(3)]
|
||||
|
||||
def autoshifted_dxyz(self) -> List[NDArray[numpy.float64]]:
|
||||
def autoshifted_dxyz(self) -> list[NDArray[numpy.float64]]:
|
||||
"""
|
||||
Return cell widths, with each dimension shifted by the corresponding shifts.
|
||||
|
||||
@ -242,7 +240,7 @@ class Grid:
|
||||
raise GridError('Autoshifting requires exactly 3 grids')
|
||||
return [self.shifted_dxyz(which_shifts=a)[a] for a in range(3)]
|
||||
|
||||
def allocate(self, fill_value: Optional[float] = 1.0, dtype=numpy.float32) -> NDArray:
|
||||
def allocate(self, fill_value: float | None = 1.0, dtype=numpy.float32) -> NDArray:
|
||||
"""
|
||||
Allocate an ndarray for storing grid data.
|
||||
|
||||
@ -263,7 +261,7 @@ class Grid:
|
||||
self,
|
||||
pixel_edge_coordinates: Sequence[ArrayLike],
|
||||
shifts: ArrayLike = Yee_Shifts_E,
|
||||
periodic: Union[bool, Sequence[bool]] = False,
|
||||
periodic: bool | Sequence[bool] = False,
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
@ -320,7 +318,7 @@ class Grid:
|
||||
g.__dict__.update(tmp_dict)
|
||||
return g
|
||||
|
||||
def save(self: T, filename: str) -> T:
|
||||
def save(self, filename: str) -> Self:
|
||||
"""
|
||||
Save to file.
|
||||
|
||||
@ -334,7 +332,7 @@ class Grid:
|
||||
pickle.dump(self.__dict__, f, protocol=2)
|
||||
return self
|
||||
|
||||
def copy(self: T) -> T:
|
||||
def copy(self) -> Self:
|
||||
"""
|
||||
Returns:
|
||||
Deep copy of the grid.
|
||||
|
@ -1,8 +1,6 @@
|
||||
"""
|
||||
Position-related methods for Grid class
|
||||
"""
|
||||
from typing import List, Optional, Sequence
|
||||
|
||||
import numpy
|
||||
from numpy.typing import NDArray, ArrayLike
|
||||
|
||||
@ -12,7 +10,7 @@ from . import GridError
|
||||
def ind2pos(
|
||||
self,
|
||||
ind: NDArray,
|
||||
which_shifts: Optional[int] = None,
|
||||
which_shifts: int | None = None,
|
||||
round_ind: bool = True,
|
||||
check_bounds: bool = True
|
||||
) -> NDArray[numpy.float64]:
|
||||
@ -64,7 +62,7 @@ def ind2pos(
|
||||
def pos2ind(
|
||||
self,
|
||||
r: ArrayLike,
|
||||
which_shifts: Optional[int],
|
||||
which_shifts: int | None,
|
||||
round_ind: bool = True,
|
||||
check_bounds: bool = True
|
||||
) -> NDArray[numpy.float64]:
|
||||
@ -101,7 +99,7 @@ def pos2ind(
|
||||
|
||||
grid_pos = numpy.zeros((3,))
|
||||
for a in range(3):
|
||||
xi = numpy.digitize(r[a], sexyz[a]) - 1 # Figure out which cell we're in
|
||||
xi = numpy.digitize(r[a], sexyz[a]) - 1 # Figure out which cell we're in
|
||||
xi_clipped = numpy.clip(xi, 0, sexyz[a].size - 2) # Clip back into grid bounds
|
||||
|
||||
# No need to interpolate if round_ind is true or we were outside the grid
|
||||
|
@ -1,13 +1,18 @@
|
||||
"""
|
||||
Readback and visualization methods for Grid class
|
||||
"""
|
||||
from typing import Dict, Optional, Union, Any
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
import numpy
|
||||
from numpy.typing import NDArray, ArrayLike
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from . import GridError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import matplotlib.axes
|
||||
import matplotlib.figure
|
||||
|
||||
|
||||
# .visualize_* uses matplotlib
|
||||
# .visualize_isosurface uses skimage
|
||||
# .visualize_isosurface uses mpl_toolkits.mplot3d
|
||||
@ -85,8 +90,8 @@ def visualize_slice(
|
||||
which_shifts: int = 0,
|
||||
sample_period: int = 1,
|
||||
finalize: bool = True,
|
||||
pcolormesh_args: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
pcolormesh_args: dict[str, Any] | None = None,
|
||||
) -> tuple['matplotlib.axes.Axes', 'matplotlib.figure.Figure']:
|
||||
"""
|
||||
Visualize a slice of a grid.
|
||||
Interpolates if given a position between two planes.
|
||||
@ -97,6 +102,9 @@ def visualize_slice(
|
||||
which_shifts: Which grid to display. Default is the first grid (0).
|
||||
sample_period: Period for down-sampling the image. Default 1 (disabled)
|
||||
finalize: Whether to call `pyplot.show()` after constructing the plot. Default `True`
|
||||
|
||||
Returns:
|
||||
(Figure, Axes)
|
||||
"""
|
||||
from matplotlib import pyplot
|
||||
|
||||
@ -115,25 +123,27 @@ def visualize_slice(
|
||||
xmesh, ymesh = numpy.meshgrid(x, y, indexing='ij')
|
||||
x_label, y_label = ('xyz'[a] for a in surface)
|
||||
|
||||
pyplot.figure()
|
||||
pyplot.pcolormesh(xmesh, ymesh, grid_slice, **pcolormesh_args)
|
||||
pyplot.colorbar()
|
||||
pyplot.gca().set_aspect('equal', adjustable='box')
|
||||
pyplot.xlabel(x_label)
|
||||
pyplot.ylabel(y_label)
|
||||
fig, ax = pyplot.subplots()
|
||||
mappable = ax.pcolormesh(xmesh, ymesh, grid_slice, **pcolormesh_args)
|
||||
fig.colorbar(mappable)
|
||||
ax.set_aspect('equal', adjustable='box')
|
||||
ax.set_xlabel(x_label)
|
||||
ax.set_ylabel(y_label)
|
||||
if finalize:
|
||||
pyplot.show()
|
||||
|
||||
return fig, ax
|
||||
|
||||
|
||||
def visualize_isosurface(
|
||||
self,
|
||||
cell_data: NDArray,
|
||||
level: Optional[float] = None,
|
||||
level: float | None = None,
|
||||
which_shifts: int = 0,
|
||||
sample_period: int = 1,
|
||||
show_edges: bool = True,
|
||||
finalize: bool = True,
|
||||
) -> None:
|
||||
) -> tuple['matplotlib.axes.Axes', 'matplotlib.figure.Figure']:
|
||||
"""
|
||||
Draw an isosurface plot of the device.
|
||||
|
||||
@ -144,6 +154,9 @@ def visualize_isosurface(
|
||||
sample_period: Period for down-sampling the image. Default 1 (disabled)
|
||||
show_edges: Whether to draw triangle edges. Default `True`
|
||||
finalize: Whether to call `pyplot.show()` after constructing the plot. Default `True`
|
||||
|
||||
Returns:
|
||||
(Figure, Axes)
|
||||
"""
|
||||
from matplotlib import pyplot
|
||||
import skimage.measure
|
||||
@ -185,3 +198,5 @@ def visualize_isosurface(
|
||||
|
||||
if finalize:
|
||||
pyplot.show()
|
||||
|
||||
return fig, ax
|
||||
|
@ -32,13 +32,13 @@ classifiers = [
|
||||
"Topic :: Scientific/Engineering :: Physics",
|
||||
"Topic :: Scientific/Engineering :: Visualization",
|
||||
]
|
||||
requires-python = ">=3.8"
|
||||
requires-python = ">=3.11"
|
||||
include = [
|
||||
"LICENSE.md"
|
||||
]
|
||||
dynamic = ["version"]
|
||||
dependencies = [
|
||||
"numpy~=1.21",
|
||||
"numpy~=1.26",
|
||||
"float_raster",
|
||||
]
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user