Compare commits

...

4 Commits

7 changed files with 91 additions and 51 deletions

29
.flake8 Normal file
View 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,

View File

@ -14,7 +14,7 @@ the coordinates of the boundary points along each axis).
## Installation ## Installation
Requirements: Requirements:
* python 3 (written and tested with 3.9) * python >3.11 (written and tested with 3.12)
* numpy * numpy
* [float_raster](https://mpxd.net/code/jan/float_raster) * [float_raster](https://mpxd.net/code/jan/float_raster)
* matplotlib (optional, used for visualization functions) * matplotlib (optional, used for visualization functions)

View File

@ -1,7 +1,7 @@
""" """
Drawing-related methods for Grid class Drawing-related methods for Grid class
""" """
from typing import List, Optional, Union, Sequence, Callable from typing import Union, Sequence, Callable
import numpy import numpy
from numpy.typing import NDArray, ArrayLike from numpy.typing import NDArray, ArrayLike
@ -27,7 +27,7 @@ def draw_polygons(
center: ArrayLike, center: ArrayLike,
polygons: Sequence[NDArray], polygons: Sequence[NDArray],
thickness: float, thickness: float,
foreground: Union[Sequence[foreground_t], foreground_t], foreground: Sequence[foreground_t] | foreground_t,
) -> None: ) -> None:
""" """
Draw polygons on an axis-aligned plane. Draw polygons on an axis-aligned plane.
@ -70,7 +70,7 @@ def draw_polygons(
+ 'xyz'[surface_normal]) + 'xyz'[surface_normal])
# Broadcast foreground where necessary # 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 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): elif isinstance(foreground, numpy.ndarray):
@ -113,7 +113,7 @@ def draw_polygons(
foregrounds_i = foregrounds[i] foregrounds_i = foregrounds[i]
if callable(foregrounds_i): if callable(foregrounds_i):
# meshgrid over the (shifted) domain # 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') (x0, y0, z0) = numpy.meshgrid(*domain, indexing='ij')
# evaluate on the meshgrid # evaluate on the meshgrid
@ -202,7 +202,7 @@ def draw_polygon(
center: ArrayLike, center: ArrayLike,
polygon: ArrayLike, polygon: ArrayLike,
thickness: float, thickness: float,
foreground: Union[Sequence[foreground_t], foreground_t], foreground: Sequence[foreground_t] | foreground_t,
) -> None: ) -> None:
""" """
Draw a polygon on an axis-aligned plane. Draw a polygon on an axis-aligned plane.
@ -226,7 +226,7 @@ def draw_slab(
surface_normal: int, surface_normal: int,
center: ArrayLike, center: ArrayLike,
thickness: float, thickness: float,
foreground: Union[Sequence[foreground_t], foreground_t], foreground: Sequence[foreground_t] | foreground_t,
) -> None: ) -> None:
""" """
Draw an axis-aligned infinite slab. Draw an axis-aligned infinite slab.
@ -276,7 +276,7 @@ def draw_cuboid(
cell_data: NDArray, cell_data: NDArray,
center: ArrayLike, center: ArrayLike,
dimensions: ArrayLike, dimensions: ArrayLike,
foreground: Union[Sequence[foreground_t], foreground_t], foreground: Sequence[foreground_t] | foreground_t,
) -> None: ) -> None:
""" """
Draw an axis-aligned cuboid Draw an axis-aligned cuboid
@ -305,7 +305,7 @@ def draw_cylinder(
radius: float, radius: float,
thickness: float, thickness: float,
num_points: int, num_points: int,
foreground: Union[Sequence[foreground_t], foreground_t], foreground: Sequence[foreground_t] | foreground_t,
) -> None: ) -> None:
""" """
Draw an axis-aligned cylinder. Approximated by a num_points-gon 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 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. 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) x = radius * numpy.sin(theta)
y = radius * numpy.cos(theta) y = radius * numpy.cos(theta)
polygon = numpy.hstack((x[:, None], y[:, None])) polygon = numpy.hstack((x[:, None], y[:, None]))
@ -360,8 +360,8 @@ def draw_extrude_rectangle(
surface = numpy.delete(range(3), direction) surface = numpy.delete(range(3), direction)
dim = numpy.fabs(numpy.diff(rectangle, axis=0).T)[surface] 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, 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]/2.0)).T numpy.array([-1, 1, 1, -1], dtype=float) * dim[1] * 0.5)).T
thickness = distance thickness = distance
foreground_func = [] 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)] ind = [int(numpy.floor(z)) if i == direction else slice(None) for i in range(3)]
fpart = z - numpy.floor(z) 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)] foreground = mult[0] * grid[tuple(ind)]
ind[direction] += 1 # type: ignore #(known safe) ind[direction] += 1 # type: ignore #(known safe)

View File

@ -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 import numpy
from numpy.typing import NDArray, ArrayLike from numpy.typing import NDArray, ArrayLike
from numpy import diff, floor, ceil, zeros, hstack, newaxis
import pickle import pickle
import warnings import warnings
@ -12,7 +11,6 @@ from . import GridError
foreground_callable_type = Callable[[NDArray, NDArray, NDArray], NDArray] foreground_callable_type = Callable[[NDArray, NDArray, NDArray], NDArray]
T = TypeVar('T', bound='Grid')
class 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 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. 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.""" """Cell edges. Monotonically increasing without duplicates."""
periodic: List[bool] periodic: list[bool]
"""For each axis, determines how far the rightmost boundary gets shifted. """ """For each axis, determines how far the rightmost boundary gets shifted. """
shifts: NDArray shifts: NDArray
@ -80,7 +78,7 @@ class Grid:
from .position import ind2pos, pos2ind from .position import ind2pos, pos2ind
@property @property
def dxyz(self) -> List[NDArray]: def dxyz(self) -> list[NDArray]:
""" """
Cell sizes for each axis, no shifts applied Cell sizes for each axis, no shifts applied
@ -90,7 +88,7 @@ class Grid:
return [numpy.diff(ee) for ee in self.exyz] return [numpy.diff(ee) for ee in self.exyz]
@property @property
def xyz(self) -> List[NDArray]: def xyz(self) -> list[NDArray]:
""" """
Cell centers for each axis, no shifts applied Cell centers for each axis, no shifts applied
@ -124,7 +122,7 @@ class Grid:
return numpy.hstack((self.num_grids, self.shape)) return numpy.hstack((self.num_grids, self.shape))
@property @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 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 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) return numpy.array(centers, dtype=float)
@property @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 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 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) d_max = numpy.array([max(self.dxyz[a]) for a in range(3)], dtype=float)
return d_min, d_max 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. Returns edges for which_shifts.
@ -188,7 +186,7 @@ class Grid:
return [self.exyz[a] + dxyz[a] * shifts[a] for a in range(3)] 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`. Returns cell sizes for `which_shifts`.
@ -215,7 +213,7 @@ class Grid:
return sdxyz 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`. Returns cell centers for `which_shifts`.
@ -231,7 +229,7 @@ class Grid:
dxyz = self.shifted_dxyz(which_shifts) dxyz = self.shifted_dxyz(which_shifts)
return [exyz[a][:-1] + dxyz[a] / 2.0 for a in range(3)] 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. Return cell widths, with each dimension shifted by the corresponding shifts.
@ -242,7 +240,7 @@ class Grid:
raise GridError('Autoshifting requires exactly 3 grids') raise GridError('Autoshifting requires exactly 3 grids')
return [self.shifted_dxyz(which_shifts=a)[a] for a in range(3)] 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. Allocate an ndarray for storing grid data.
@ -263,7 +261,7 @@ class Grid:
self, self,
pixel_edge_coordinates: Sequence[ArrayLike], pixel_edge_coordinates: Sequence[ArrayLike],
shifts: ArrayLike = Yee_Shifts_E, shifts: ArrayLike = Yee_Shifts_E,
periodic: Union[bool, Sequence[bool]] = False, periodic: bool | Sequence[bool] = False,
) -> None: ) -> None:
""" """
Args: Args:
@ -320,7 +318,7 @@ class Grid:
g.__dict__.update(tmp_dict) g.__dict__.update(tmp_dict)
return g return g
def save(self: T, filename: str) -> T: def save(self, filename: str) -> Self:
""" """
Save to file. Save to file.
@ -334,7 +332,7 @@ class Grid:
pickle.dump(self.__dict__, f, protocol=2) pickle.dump(self.__dict__, f, protocol=2)
return self return self
def copy(self: T) -> T: def copy(self) -> Self:
""" """
Returns: Returns:
Deep copy of the grid. Deep copy of the grid.

View File

@ -1,8 +1,6 @@
""" """
Position-related methods for Grid class Position-related methods for Grid class
""" """
from typing import List, Optional, Sequence
import numpy import numpy
from numpy.typing import NDArray, ArrayLike from numpy.typing import NDArray, ArrayLike
@ -12,7 +10,7 @@ from . import GridError
def ind2pos( def ind2pos(
self, self,
ind: NDArray, ind: NDArray,
which_shifts: Optional[int] = None, which_shifts: int | None = None,
round_ind: bool = True, round_ind: bool = True,
check_bounds: bool = True check_bounds: bool = True
) -> NDArray[numpy.float64]: ) -> NDArray[numpy.float64]:
@ -64,7 +62,7 @@ def ind2pos(
def pos2ind( def pos2ind(
self, self,
r: ArrayLike, r: ArrayLike,
which_shifts: Optional[int], which_shifts: int | None,
round_ind: bool = True, round_ind: bool = True,
check_bounds: bool = True check_bounds: bool = True
) -> NDArray[numpy.float64]: ) -> NDArray[numpy.float64]:

View File

@ -1,13 +1,18 @@
""" """
Readback and visualization methods for Grid class Readback and visualization methods for Grid class
""" """
from typing import Dict, Optional, Union, Any from typing import Any, TYPE_CHECKING
import numpy import numpy
from numpy.typing import NDArray, ArrayLike from numpy.typing import NDArray
from . import GridError from . import GridError
if TYPE_CHECKING:
import matplotlib.axes
import matplotlib.figure
# .visualize_* uses matplotlib # .visualize_* uses matplotlib
# .visualize_isosurface uses skimage # .visualize_isosurface uses skimage
# .visualize_isosurface uses mpl_toolkits.mplot3d # .visualize_isosurface uses mpl_toolkits.mplot3d
@ -85,8 +90,8 @@ def visualize_slice(
which_shifts: int = 0, which_shifts: int = 0,
sample_period: int = 1, sample_period: int = 1,
finalize: bool = True, finalize: bool = True,
pcolormesh_args: Optional[Dict[str, Any]] = None, pcolormesh_args: dict[str, Any] | None = None,
) -> None: ) -> tuple['matplotlib.axes.Axes', 'matplotlib.figure.Figure']:
""" """
Visualize a slice of a grid. Visualize a slice of a grid.
Interpolates if given a position between two planes. 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). which_shifts: Which grid to display. Default is the first grid (0).
sample_period: Period for down-sampling the image. Default 1 (disabled) sample_period: Period for down-sampling the image. Default 1 (disabled)
finalize: Whether to call `pyplot.show()` after constructing the plot. Default `True` finalize: Whether to call `pyplot.show()` after constructing the plot. Default `True`
Returns:
(Figure, Axes)
""" """
from matplotlib import pyplot from matplotlib import pyplot
@ -115,25 +123,27 @@ def visualize_slice(
xmesh, ymesh = numpy.meshgrid(x, y, indexing='ij') xmesh, ymesh = numpy.meshgrid(x, y, indexing='ij')
x_label, y_label = ('xyz'[a] for a in surface) x_label, y_label = ('xyz'[a] for a in surface)
pyplot.figure() fig, ax = pyplot.subplots()
pyplot.pcolormesh(xmesh, ymesh, grid_slice, **pcolormesh_args) mappable = ax.pcolormesh(xmesh, ymesh, grid_slice, **pcolormesh_args)
pyplot.colorbar() fig.colorbar(mappable)
pyplot.gca().set_aspect('equal', adjustable='box') ax.set_aspect('equal', adjustable='box')
pyplot.xlabel(x_label) ax.set_xlabel(x_label)
pyplot.ylabel(y_label) ax.set_ylabel(y_label)
if finalize: if finalize:
pyplot.show() pyplot.show()
return fig, ax
def visualize_isosurface( def visualize_isosurface(
self, self,
cell_data: NDArray, cell_data: NDArray,
level: Optional[float] = None, level: float | None = None,
which_shifts: int = 0, which_shifts: int = 0,
sample_period: int = 1, sample_period: int = 1,
show_edges: bool = True, show_edges: bool = True,
finalize: bool = True, finalize: bool = True,
) -> None: ) -> tuple['matplotlib.axes.Axes', 'matplotlib.figure.Figure']:
""" """
Draw an isosurface plot of the device. 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) sample_period: Period for down-sampling the image. Default 1 (disabled)
show_edges: Whether to draw triangle edges. Default `True` show_edges: Whether to draw triangle edges. Default `True`
finalize: Whether to call `pyplot.show()` after constructing the plot. Default `True` finalize: Whether to call `pyplot.show()` after constructing the plot. Default `True`
Returns:
(Figure, Axes)
""" """
from matplotlib import pyplot from matplotlib import pyplot
import skimage.measure import skimage.measure
@ -185,3 +198,5 @@ def visualize_isosurface(
if finalize: if finalize:
pyplot.show() pyplot.show()
return fig, ax

View File

@ -32,13 +32,13 @@ classifiers = [
"Topic :: Scientific/Engineering :: Physics", "Topic :: Scientific/Engineering :: Physics",
"Topic :: Scientific/Engineering :: Visualization", "Topic :: Scientific/Engineering :: Visualization",
] ]
requires-python = ">=3.8" requires-python = ">=3.11"
include = [ include = [
"LICENSE.md" "LICENSE.md"
] ]
dynamic = ["version"] dynamic = ["version"]
dependencies = [ dependencies = [
"numpy~=1.21", "numpy~=1.26",
"float_raster", "float_raster",
] ]