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
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)

View File

@ -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)

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
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.

View File

@ -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

View File

@ -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

View File

@ -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",
]