Compare commits

...

4 Commits

@ -1,2 +0,0 @@
include README.md
include LICENSE.md

@ -0,0 +1 @@
../LICENSE.md

@ -0,0 +1 @@
../README.md

@ -1,4 +0,0 @@
""" VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """
__version__ = '''
1.0
'''.strip()

@ -19,6 +19,5 @@ from .error import GridError
from .grid import Grid
__author__ = 'Jan Petykiewicz'
from .VERSION import __version__
__version__ = '1.1'
version = __version__

@ -3,7 +3,8 @@ Drawing-related methods for Grid class
"""
from typing import List, Optional, Union, Sequence, Callable
import numpy # type: ignore
import numpy
from numpy.typing import NDArray, ArrayLike
from float_raster import raster
from . import GridError
@ -15,17 +16,19 @@ from . import GridError
# without having to pass `cell_data` again each time?
foreground_callable_t = Callable[[numpy.ndarray, numpy.ndarray, numpy.ndarray], numpy.ndarray]
foreground_callable_t = Callable[[NDArray, NDArray, NDArray], NDArray]
foreground_t = Union[float, foreground_callable_t]
def draw_polygons(self,
cell_data: numpy.ndarray,
surface_normal: int,
center: numpy.ndarray,
polygons: Sequence[numpy.ndarray],
thickness: float,
foreground: Union[Sequence[Union[float, foreground_callable_t]], float, foreground_callable_t],
) -> None:
def draw_polygons(
self,
cell_data: NDArray,
surface_normal: int,
center: ArrayLike,
polygons: Sequence[NDArray],
thickness: float,
foreground: Union[Sequence[foreground_t], foreground_t],
) -> None:
"""
Draw polygons on an axis-aligned plane.
@ -67,15 +70,18 @@ def draw_polygons(self,
+ 'xyz'[surface_normal])
# Broadcast foreground where necessary
if numpy.size(foreground) == 1:
foreground = [foreground] * len(cell_data)
foregrounds: Union[Sequence[foreground_callable_t], Sequence[float]]
if numpy.size(foreground) == 1: # type: ignore
foregrounds = [foreground] * len(cell_data) # type: ignore
elif isinstance(foreground, numpy.ndarray):
raise GridError('ndarray not supported for foreground')
else:
foregrounds = foreground # type: ignore
# ## Compute sub-domain of the grid occupied by polygons
# 1) Compute outer bounds (bd) of polygons
bd_2d_min = [0, 0]
bd_2d_max = [0, 0]
bd_2d_min = numpy.array([0, 0])
bd_2d_max = numpy.array([0, 0])
for polygon in polygons:
bd_2d_min = numpy.minimum(bd_2d_min, polygon.min(axis=0))
bd_2d_max = numpy.maximum(bd_2d_max, polygon.max(axis=0))
@ -97,27 +103,28 @@ def draw_polygons(self,
polygons = [poly + center[surface] for poly in polygons]
# ## Generate weighing function
def to_3d(vector: numpy.ndarray, val: float = 0.0) -> numpy.ndarray:
def to_3d(vector: NDArray, val: float = 0.0) -> NDArray[numpy.float64]:
v_2d = numpy.array(vector, dtype=float)
return numpy.insert(v_2d, surface_normal, (val,))
# iterate over grids
for i, grid in enumerate(cell_data):
# ## Evaluate or expand foreground[i]
if callable(foreground[i]):
for i, _ in enumerate(cell_data):
# ## Evaluate or expand foregrounds[i]
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)]
(x0, y0, z0) = numpy.meshgrid(*domain, indexing='ij')
# evaluate on the meshgrid
foreground_i = foreground[i](x0, y0, z0)
if not numpy.isfinite(foreground_i).all():
foreground_val = foregrounds_i(x0, y0, z0)
if not numpy.isfinite(foreground_val).all():
raise GridError(f'Non-finite values in foreground[{i}]')
elif numpy.size(foreground[i]) != 1:
raise GridError(f'Unsupported foreground[{i}]: {type(foreground[i])}')
elif numpy.size(foregrounds_i) != 1:
raise GridError(f'Unsupported foreground[{i}]: {type(foregrounds_i)}')
else:
# foreground[i] is scalar non-callable
foreground_i = foreground[i]
foreground_val = foregrounds_i
w_xy = numpy.zeros((bdi_max - bdi_min + 1)[surface].astype(int))
@ -185,17 +192,18 @@ def draw_polygons(self,
# ## Modify the grid
g_slice = (i,) + tuple(numpy.s_[bdi_min[a]:bdi_max[a] + 1] for a in range(3))
cell_data[g_slice] = (1 - w) * cell_data[g_slice] + w * foreground_i
cell_data[g_slice] = (1 - w) * cell_data[g_slice] + w * foreground_val
def draw_polygon(self,
cell_data: numpy.ndarray,
surface_normal: int,
center: numpy.ndarray,
polygon: numpy.ndarray,
thickness: float,
foreground: Union[Sequence[Union[float, foreground_callable_t]], float, foreground_callable_t],
) -> None:
def draw_polygon(
self,
cell_data: NDArray,
surface_normal: int,
center: ArrayLike,
polygon: ArrayLike,
thickness: float,
foreground: Union[Sequence[foreground_t], foreground_t],
) -> None:
"""
Draw a polygon on an axis-aligned plane.
@ -212,13 +220,14 @@ def draw_polygon(self,
self.draw_polygons(cell_data, surface_normal, center, [polygon], thickness, foreground)
def draw_slab(self,
cell_data: numpy.ndarray,
surface_normal: int,
center: numpy.ndarray,
thickness: float,
foreground: Union[List[Union[float, foreground_callable_t]], float, foreground_callable_t],
) -> None:
def draw_slab(
self,
cell_data: NDArray,
surface_normal: int,
center: ArrayLike,
thickness: float,
foreground: Union[Sequence[foreground_t], foreground_t],
) -> None:
"""
Draw an axis-aligned infinite slab.
@ -262,12 +271,13 @@ def draw_slab(self,
self.draw_polygon(cell_data, surface_normal, center_shift, p, thickness, foreground)
def draw_cuboid(self,
cell_data: numpy.ndarray,
center: numpy.ndarray,
dimensions: numpy.ndarray,
foreground: Union[List[Union[float, foreground_callable_t]], float, foreground_callable_t],
) -> None:
def draw_cuboid(
self,
cell_data: NDArray,
center: ArrayLike,
dimensions: ArrayLike,
foreground: Union[Sequence[foreground_t], foreground_t],
) -> None:
"""
Draw an axis-aligned cuboid
@ -278,6 +288,7 @@ def draw_cuboid(self,
sizes of the cuboid
foreground: Value to draw with ('brush color'). See `draw_polygons()` for details.
"""
dimensions = numpy.array(dimensions, copy=False)
p = numpy.array([[-dimensions[0], +dimensions[1]],
[+dimensions[0], +dimensions[1]],
[+dimensions[0], -dimensions[1]],
@ -286,15 +297,16 @@ def draw_cuboid(self,
self.draw_polygon(cell_data, 2, center, p, thickness, foreground)
def draw_cylinder(self,
cell_data: numpy.ndarray,
surface_normal: int,
center: numpy.ndarray,
radius: float,
thickness: float,
num_points: int,
foreground: Union[List[Union[float, foreground_callable_t]], float, foreground_callable_t],
) -> None:
def draw_cylinder(
self,
cell_data: NDArray,
surface_normal: int,
center: ArrayLike,
radius: float,
thickness: float,
num_points: int,
foreground: Union[Sequence[foreground_t], foreground_t],
) -> None:
"""
Draw an axis-aligned cylinder. Approximated by a num_points-gon
@ -314,13 +326,14 @@ def draw_cylinder(self,
self.draw_polygon(cell_data, surface_normal, center, polygon, thickness, foreground)
def draw_extrude_rectangle(self,
cell_data: numpy.ndarray,
rectangle: numpy.ndarray,
direction: int,
polarity: int,
distance: float,
) -> None:
def draw_extrude_rectangle(
self,
cell_data: NDArray,
rectangle: ArrayLike,
direction: int,
polarity: int,
distance: float,
) -> None:
"""
Extrude a rectangle of a previously-drawn structure along an axis.
@ -361,10 +374,10 @@ def draw_extrude_rectangle(self,
mult = [1-fpart, fpart][::s] # reverses if s negative
foreground = mult[0] * grid[tuple(ind)]
ind[direction] += 1
ind[direction] += 1 # type: ignore #(known safe)
foreground += mult[1] * grid[tuple(ind)]
def f_foreground(xs, ys, zs, i=i, foreground=foreground) -> numpy.ndarray:
def f_foreground(xs, ys, zs, i=i, foreground=foreground) -> NDArray[numpy.int_]:
# transform from natural position to index
xyzi = numpy.array([self.pos2ind(qrs, which_shifts=i)
for qrs in zip(xs.flat, ys.flat, zs.flat)], dtype=int)

@ -1,4 +1,4 @@
import numpy # type: ignore
import numpy
from gridlock import Grid

@ -1,6 +1,7 @@
from typing import List, Tuple, Callable, Dict, Optional, Union, Sequence, ClassVar, TypeVar
import numpy # type: ignore
import numpy
from numpy.typing import NDArray, ArrayLike
from numpy import diff, floor, ceil, zeros, hstack, newaxis
import pickle
@ -10,7 +11,7 @@ import copy
from . import GridError
foreground_callable_type = Callable[[numpy.ndarray, numpy.ndarray, numpy.ndarray], numpy.ndarray]
foreground_callable_type = Callable[[NDArray, NDArray, NDArray], NDArray]
T = TypeVar('T', bound='Grid')
@ -48,23 +49,27 @@ 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[numpy.ndarray]
exyz: List[NDArray]
"""Cell edges. Monotonically increasing without duplicates."""
periodic: List[bool]
"""For each axis, determines how far the rightmost boundary gets shifted. """
shifts: numpy.ndarray
shifts: NDArray
"""Offsets `[[x0, y0, z0], [x1, y1, z1], ...]` for grid `0,1,...`"""
Yee_Shifts_E: ClassVar[numpy.ndarray] = 0.5 * numpy.array([[1, 0, 0],
[0, 1, 0],
[0, 0, 1]], dtype=float)
Yee_Shifts_E: ClassVar[NDArray] = 0.5 * numpy.array([
[1, 0, 0],
[0, 1, 0],
[0, 0, 1],
], dtype=float)
"""Default shifts for Yee grid E-field"""
Yee_Shifts_H: ClassVar[numpy.ndarray] = 0.5 * numpy.array([[0, 1, 1],
[1, 0, 1],
[1, 1, 0]], dtype=float)
Yee_Shifts_H: ClassVar[NDArray] = 0.5 * numpy.array([
[0, 1, 1],
[1, 0, 1],
[1, 1, 0],
], dtype=float)
"""Default shifts for Yee grid H-field"""
from .draw import (
@ -75,7 +80,7 @@ class Grid:
from .position import ind2pos, pos2ind
@property
def dxyz(self) -> List[numpy.ndarray]:
def dxyz(self) -> List[NDArray]:
"""
Cell sizes for each axis, no shifts applied
@ -85,7 +90,7 @@ class Grid:
return [numpy.diff(ee) for ee in self.exyz]
@property
def xyz(self) -> List[numpy.ndarray]:
def xyz(self) -> List[NDArray]:
"""
Cell centers for each axis, no shifts applied
@ -95,7 +100,7 @@ class Grid:
return [self.exyz[a][:-1] + self.dxyz[a] / 2.0 for a in range(3)]
@property
def shape(self) -> numpy.ndarray:
def shape(self) -> NDArray[numpy.int_]:
"""
The number of cells in x, y, and z
@ -119,7 +124,7 @@ class Grid:
return numpy.hstack((self.num_grids, self.shape))
@property
def dxyz_with_ghost(self) -> List[numpy.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
@ -135,7 +140,7 @@ class Grid:
return [numpy.hstack((self.dxyz[a], self.dxyz[a][e])) for a, e in zip(range(3), el)]
@property
def center(self) -> numpy.ndarray:
def center(self) -> NDArray[numpy.float64]:
"""
Center position of the entire grid, no shifts applied
@ -148,7 +153,7 @@ class Grid:
return numpy.array(centers, dtype=float)
@property
def dxyz_limits(self) -> Tuple[numpy.ndarray, numpy.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
@ -161,7 +166,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[numpy.ndarray]:
def shifted_exyz(self, which_shifts: Optional[int]) -> List[NDArray]:
"""
Returns edges for which_shifts.
@ -183,7 +188,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[numpy.ndarray]:
def shifted_dxyz(self, which_shifts: Optional[int]) -> List[NDArray]:
"""
Returns cell sizes for `which_shifts`.
@ -210,7 +215,7 @@ class Grid:
return sdxyz
def shifted_xyz(self, which_shifts: Optional[int]) -> List[numpy.ndarray]:
def shifted_xyz(self, which_shifts: Optional[int]) -> List[NDArray[numpy.float64]]:
"""
Returns cell centers for `which_shifts`.
@ -226,7 +231,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[numpy.ndarray]:
def autoshifted_dxyz(self) -> List[NDArray[numpy.float64]]:
"""
Return cell widths, with each dimension shifted by the corresponding shifts.
@ -237,7 +242,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) -> numpy.ndarray:
def allocate(self, fill_value: Optional[float] = 1.0, dtype=numpy.float32) -> NDArray:
"""
Allocate an ndarray for storing grid data.
@ -254,11 +259,12 @@ class Grid:
else:
return numpy.full(self.cell_data_shape, fill_value, dtype=dtype)
def __init__(self,
pixel_edge_coordinates: Sequence[numpy.ndarray],
shifts: numpy.ndarray = Yee_Shifts_E,
periodic: Union[bool, Sequence[bool]] = False,
) -> None:
def __init__(
self,
pixel_edge_coordinates: Sequence[ArrayLike],
shifts: ArrayLike = Yee_Shifts_E,
periodic: Union[bool, Sequence[bool]] = False,
) -> None:
"""
Args:
pixel_edge_coordinates: 3-element list of (ndarrays or lists) specifying the

@ -1,19 +1,21 @@
"""
Position-related methods for Grid class
"""
from typing import List, Optional
from typing import List, Optional, Sequence
import numpy # type: ignore
import numpy
from numpy.typing import NDArray, ArrayLike
from . import GridError
def ind2pos(self,
ind: numpy.ndarray,
which_shifts: Optional[int] = None,
round_ind: bool = True,
check_bounds: bool = True
) -> numpy.ndarray:
def ind2pos(
self,
ind: NDArray,
which_shifts: Optional[int] = None,
round_ind: bool = True,
check_bounds: bool = True
) -> NDArray[numpy.float64]:
"""
Returns the natural position corresponding to the specified cell center indices.
The resulting position is clipped to the bounds of the grid
@ -59,12 +61,13 @@ def ind2pos(self,
return numpy.array(position, dtype=float)
def pos2ind(self,
r: numpy.ndarray,
which_shifts: Optional[int],
round_ind: bool = True,
check_bounds: bool = True
) -> numpy.ndarray:
def pos2ind(
self,
r: ArrayLike,
which_shifts: Optional[int],
round_ind: bool = True,
check_bounds: bool = True
) -> NDArray[numpy.float64]:
"""
Returns the cell-center indices corresponding to the specified natural position.
The resulting position is clipped to within the outer centers of the grid.

@ -3,7 +3,8 @@ Readback and visualization methods for Grid class
"""
from typing import Dict, Optional, Union, Any
import numpy # type: ignore
import numpy
from numpy.typing import NDArray, ArrayLike
from . import GridError
@ -12,13 +13,14 @@ from . import GridError
# .visualize_isosurface uses mpl_toolkits.mplot3d
def get_slice(self,
cell_data: numpy.ndarray,
surface_normal: int,
center: float,
which_shifts: int = 0,
sample_period: int = 1
) -> numpy.ndarray:
def get_slice(
self,
cell_data: NDArray,
surface_normal: int,
center: float,
which_shifts: int = 0,
sample_period: int = 1
) -> NDArray:
"""
Retrieve a slice of a grid.
Interpolates if given a position between two planes.
@ -75,15 +77,16 @@ def get_slice(self,
return sliced_grid
def visualize_slice(self,
cell_data: numpy.ndarray,
surface_normal: int,
center: float,
which_shifts: int = 0,
sample_period: int = 1,
finalize: bool = True,
pcolormesh_args: Optional[Dict[str, Any]] = None,
) -> None:
def visualize_slice(
self,
cell_data: NDArray,
surface_normal: int,
center: float,
which_shifts: int = 0,
sample_period: int = 1,
finalize: bool = True,
pcolormesh_args: Optional[Dict[str, Any]] = None,
) -> None:
"""
Visualize a slice of a grid.
Interpolates if given a position between two planes.
@ -122,14 +125,15 @@ def visualize_slice(self,
pyplot.show()
def visualize_isosurface(self,
cell_data: numpy.ndarray,
level: Optional[float] = None,
which_shifts: int = 0,
sample_period: int = 1,
show_edges: bool = True,
finalize: bool = True,
) -> None:
def visualize_isosurface(
self,
cell_data: NDArray,
level: Optional[float] = None,
which_shifts: int = 0,
sample_period: int = 1,
show_edges: bool = True,
finalize: bool = True,
) -> None:
"""
Draw an isosurface plot of the device.

@ -1,6 +1,6 @@
import pytest # type: ignore
import numpy # type: ignore
from numpy.testing import assert_allclose, assert_array_equal # type: ignore
import numpy
from numpy.testing import assert_allclose, assert_array_equal
from .. import Grid

@ -0,0 +1,55 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "gridlock"
description = "Coupled gridding library"
readme = "README.md"
license = { file = "LICENSE.md" }
authors = [
{ name="Jan Petykiewicz", email="jan@mpxd.net" },
]
homepage = "https://mpxd.net/code/jan/gridlock"
repository = "https://mpxd.net/code/jan/gridlock"
keywords = [
"FDTD",
"gridding",
"simulation",
"nonuniform",
"FDFD",
"finite",
"difference",
]
classifiers = [
"Programming Language :: Python :: 3",
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: GNU Affero General Public License v3",
"Topic :: Multimedia :: Graphics :: 3D Rendering",
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
"Topic :: Scientific/Engineering :: Physics",
"Topic :: Scientific/Engineering :: Visualization",
]
requires-python = ">=3.8"
include = [
"LICENSE.md"
]
dynamic = ["version"]
dependencies = [
"numpy~=1.21",
"float_raster",
]
[tool.hatch.version]
path = "gridlock/__init__.py"
[project.optional-dependencies]
visualization = ["matplotlib"]
visualization-isosurface = [
"matplotlib",
"skimage>=0.13",
"mpl_toolkits",
]

@ -1,47 +0,0 @@
#!/usr/bin/env python3
from setuptools import setup, find_packages
with open('README.md', 'r') as f:
long_description = f.read()
with open('gridlock/VERSION.py', 'rt') as f:
version = f.readlines()[2].strip()
setup(name='gridlock',
version=version,
description='Coupled gridding library',
long_description=long_description,
long_description_content_type='text/markdown',
author='Jan Petykiewicz',
author_email='jan@mpxd.net',
url='https://mpxd.net/code/jan/gridlock',
packages=find_packages(),
package_data={
'gridlock': ['py.typed'],
},
install_requires=[
'numpy',
'float_raster',
],
extras_require={
'visualization': ['matplotlib'],
'visualization-isosurface': [
'matplotlib',
'skimage>=0.13',
'mpl_toolkits',
],
},
classifiers=[
'Programming Language :: Python :: 3',
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Intended Audience :: Science/Research',
'License :: OSI Approved :: GNU Affero General Public License v3',
'Topic :: Multimedia :: Graphics :: 3D Rendering',
'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)',
'Topic :: Scientific/Engineering :: Physics',
'Topic :: Scientific/Engineering :: Visualization',
],
)
Loading…
Cancel
Save