diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..0042015 --- /dev/null +++ b/.flake8 @@ -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, diff --git a/README.md b/README.md index e882b2e..e4f43d5 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/gridlock/draw.py b/gridlock/draw.py index b2a3245..75eccc6 100644 --- a/gridlock/draw.py +++ b/gridlock/draw.py @@ -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) diff --git a/gridlock/grid.py b/gridlock/grid.py index 5f132a0..d8e9653 100644 --- a/gridlock/grid.py +++ b/gridlock/grid.py @@ -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. diff --git a/gridlock/position.py b/gridlock/position.py index 35ec1df..5928174 100644 --- a/gridlock/position.py +++ b/gridlock/position.py @@ -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 diff --git a/gridlock/read.py b/gridlock/read.py index 055670c..2fe35d5 100644 --- a/gridlock/read.py +++ b/gridlock/read.py @@ -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 diff --git a/pyproject.toml b/pyproject.toml index e9ac6be..3d24be1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", ]