[fdmath] correct Gridlock Yee spacing order
Use center-to-center intervals for forward E derivatives and base-cell widths for backward H derivatives in the Gridlock examples. Document the distinction from meshes defined directly by forward intervals and fix the random fixture's backward averaging direction. Add coordinate-derivative regression coverage for graded periodic and nonperiodic Gridlock meshes using both functional and sparse operators. Validation: 1689 tests passed, 586 skipped; no new Ruff findings in changed Python files.
This commit is contained in:
parent
a08e9ce71b
commit
282ed2c38b
10 changed files with 50 additions and 9 deletions
|
|
@ -1,5 +1,18 @@
|
||||||
# fdmath
|
# fdmath
|
||||||
|
|
||||||
|
For Gridlock's default E-field Yee shifts, construct solver spacings as
|
||||||
|
`dxes = [grid.autoshifted_dxyz(), grid.dxyz]`. The forward E curl needs
|
||||||
|
distances between adjacent base-cell centers; the backward H curl needs
|
||||||
|
distances between adjacent upper cell edges. Reversing these two width families
|
||||||
|
introduces an error on graded grids that uniform grids hide.
|
||||||
|
|
||||||
|
If constructing the mesh directly from **forward derivative intervals** `d`,
|
||||||
|
with H samples at their midpoints, the backward intervals instead follow
|
||||||
|
`(d + numpy.roll(d, 1)) / 2` for periodic boundaries. These `d` arrays are
|
||||||
|
already center-to-center distances, whereas `grid.dxyz` contains base-cell
|
||||||
|
volume widths. Applying this averaging rule to `grid.dxyz` does not fix the
|
||||||
|
ordering error above.
|
||||||
|
|
||||||
::: meanas.fdmath
|
::: meanas.fdmath
|
||||||
|
|
||||||
## Functional and sparse operators
|
## Functional and sparse operators
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ def build_geometry(
|
||||||
z=Extent(min=0, max=1e6),
|
z=Extent(min=0, max=1e6),
|
||||||
)
|
)
|
||||||
|
|
||||||
dxes = [grid.dxyz, grid.autoshifted_dxyz()]
|
dxes = [grid.autoshifted_dxyz(), grid.dxyz]
|
||||||
dxes_2d = [[d[0], d[1]] for d in dxes]
|
dxes_2d = [[d[0], d[1]] for d in dxes]
|
||||||
return grid, epsilon, dxes_2d, omega
|
return grid, epsilon, dxes_2d, omega
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ def build_geometry(
|
||||||
z=Extent(min=-1e6, center=0),
|
z=Extent(min=-1e6, center=0),
|
||||||
)
|
)
|
||||||
|
|
||||||
dxes = [grid.dxyz, grid.autoshifted_dxyz()]
|
dxes = [grid.autoshifted_dxyz(), grid.dxyz]
|
||||||
dxes_2d = [[d[0], d[1]] for d in dxes]
|
dxes_2d = [[d[0], d[1]] for d in dxes]
|
||||||
return grid, epsilon, dxes_2d, omega
|
return grid, epsilon, dxes_2d, omega
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ def test0(solver=generic_solver):
|
||||||
num_points = 24,
|
num_points = 24,
|
||||||
)
|
)
|
||||||
|
|
||||||
dxes = [grid.dxyz, grid.autoshifted_dxyz()]
|
dxes = [grid.autoshifted_dxyz(), grid.dxyz]
|
||||||
for a in (0, 1, 2):
|
for a in (0, 1, 2):
|
||||||
for p in (-1, 1):
|
for p in (-1, 1):
|
||||||
dxes = meanas.fdfd.scpml.stretch_with_scpml(dxes, axis=a, polarity=p, omega=omega,
|
dxes = meanas.fdfd.scpml.stretch_with_scpml(dxes, axis=a, polarity=p, omega=omega,
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ def test1(solver=generic_solver):
|
||||||
epsilon = grid.allocate(n_air**2, dtype=numpy.float32)
|
epsilon = grid.allocate(n_air**2, dtype=numpy.float32)
|
||||||
grid.draw_cuboid(epsilon, x=dict(center=0, span=8e3), y=dict(center=0, span=w), z=dict(center=0, span=th), foreground=n_wg**2)
|
grid.draw_cuboid(epsilon, x=dict(center=0, span=8e3), y=dict(center=0, span=w), z=dict(center=0, span=th), foreground=n_wg**2)
|
||||||
|
|
||||||
dxes = [grid.dxyz, grid.autoshifted_dxyz()]
|
dxes = [grid.autoshifted_dxyz(), grid.dxyz]
|
||||||
for a in (0, 1, 2):
|
for a in (0, 1, 2):
|
||||||
for p in (-1, 1):
|
for p in (-1, 1):
|
||||||
dxes = scpml.stretch_with_scpml(dxes,omega=omega, axis=a, polarity=p,
|
dxes = scpml.stretch_with_scpml(dxes,omega=omega, axis=a, polarity=p,
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ def main() -> None:
|
||||||
ee = numpy.zeros_like(epsilon, dtype=complex)
|
ee = numpy.zeros_like(epsilon, dtype=complex)
|
||||||
hh = numpy.zeros_like(epsilon, dtype=complex)
|
hh = numpy.zeros_like(epsilon, dtype=complex)
|
||||||
|
|
||||||
dxes = [grid.dxyz, grid.autoshifted_dxyz()]
|
dxes = [grid.autoshifted_dxyz(), grid.dxyz]
|
||||||
|
|
||||||
# PMLs in every direction
|
# PMLs in every direction
|
||||||
pml_params = [
|
pml_params = [
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ def test1(solver=generic_solver):
|
||||||
epsilon = grid.allocate(n_air**2, dtype=numpy.float32)
|
epsilon = grid.allocate(n_air**2, dtype=numpy.float32)
|
||||||
grid.draw_cuboid(epsilon, center=center, dimensions=[8e3, w, th], foreground=n_wg**2)
|
grid.draw_cuboid(epsilon, center=center, dimensions=[8e3, w, th], foreground=n_wg**2)
|
||||||
|
|
||||||
dxes = [grid.dxyz, grid.autoshifted_dxyz()]
|
dxes = [grid.autoshifted_dxyz(), grid.dxyz]
|
||||||
for a in (1, 2):
|
for a in (1, 2):
|
||||||
for p in (-1, 1):
|
for p in (-1, 1):
|
||||||
dxes = scpml.stretch_with_scpml(
|
dxes = scpml.stretch_with_scpml(
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ def main(
|
||||||
|
|
||||||
# Add SCPML stretching to the FDFD grid; this matches the CPML-backed FDTD
|
# Add SCPML stretching to the FDFD grid; this matches the CPML-backed FDTD
|
||||||
# run below so the two solvers see the same absorbing boundary profile.
|
# run below so the two solvers see the same absorbing boundary profile.
|
||||||
dxes = [grid.dxyz, grid.autoshifted_dxyz()]
|
dxes = [grid.autoshifted_dxyz(), grid.dxyz]
|
||||||
for a in (0, 1, 2):
|
for a in (0, 1, 2):
|
||||||
for p in (-1, 1):
|
for p in (-1, 1):
|
||||||
dxes = scpml.stretch_with_scpml(dxes, omega=omega, axis=a, polarity=p, thickness=pml_thickness)
|
dxes = scpml.stretch_with_scpml(dxes, omega=omega, axis=a, polarity=p, thickness=pml_thickness)
|
||||||
|
|
@ -269,7 +269,7 @@ def main2():
|
||||||
ee = numpy.zeros_like(epsilon, dtype=complex)
|
ee = numpy.zeros_like(epsilon, dtype=complex)
|
||||||
hh = numpy.zeros_like(epsilon, dtype=complex)
|
hh = numpy.zeros_like(epsilon, dtype=complex)
|
||||||
|
|
||||||
dxes = [grid.dxyz, grid.autoshifted_dxyz()]
|
dxes = [grid.autoshifted_dxyz(), grid.dxyz]
|
||||||
|
|
||||||
# PMLs in every direction
|
# PMLs in every direction
|
||||||
pml_params = [
|
pml_params = [
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,6 @@ def dxes(
|
||||||
dxes[eh][ax][dxes[eh][ax].size // 2] *= 1.1
|
dxes[eh][ax][dxes[eh][ax].size // 2] *= 1.1
|
||||||
elif request.param == 'random':
|
elif request.param == 'random':
|
||||||
dxe = [prng.uniform(low=1.0 * dx, high=1.1 * dx, size=s) for s in shape[1:]]
|
dxe = [prng.uniform(low=1.0 * dx, high=1.1 * dx, size=s) for s in shape[1:]]
|
||||||
dxh = [(d + numpy.roll(d, -1)) / 2 for d in dxe]
|
dxh = [(d + numpy.roll(d, 1)) / 2 for d in dxe]
|
||||||
dxes = [dxe, dxh]
|
dxes = [dxe, dxh]
|
||||||
return dxes
|
return dxes
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import numpy
|
import numpy
|
||||||
|
import pytest
|
||||||
|
from gridlock import Grid
|
||||||
|
|
||||||
from ..fdmath import functional as fd_functional
|
from ..fdmath import functional as fd_functional
|
||||||
from ..fdmath import operators as fd_operators
|
from ..fdmath import operators as fd_operators
|
||||||
|
|
@ -58,3 +60,29 @@ def test_curls_match_sparse_operators_on_nonuniform_grid() -> None:
|
||||||
|
|
||||||
assert_fields_close(curl_forward, matrix_forward, atol=1e-12, rtol=1e-12)
|
assert_fields_close(curl_forward, matrix_forward, atol=1e-12, rtol=1e-12)
|
||||||
assert_fields_close(curl_back, matrix_back, atol=1e-12, rtol=1e-12)
|
assert_fields_close(curl_back, matrix_back, atol=1e-12, rtol=1e-12)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('periodic', [True, False])
|
||||||
|
def test_gridlock_spacings_differentiate_physical_coordinates(periodic) -> None:
|
||||||
|
grid = Grid([[0, 1, 3, 7, 15], [-2, 0, 5, 6], [3, 6, 10]], periodic=periodic)
|
||||||
|
# Transverse E samples lie at cell centers and H samples at upper cell edges.
|
||||||
|
dxes = [grid.autoshifted_dxyz(), grid.dxyz]
|
||||||
|
for forward in (True, False):
|
||||||
|
if forward:
|
||||||
|
coordinates = grid.xyz
|
||||||
|
functions = fd_functional.deriv_forward(dxes[0])
|
||||||
|
matrices = fd_operators.deriv_forward(dxes[0])
|
||||||
|
else:
|
||||||
|
coordinates = [edges[1:] for edges in grid.exyz]
|
||||||
|
functions = fd_functional.deriv_back(dxes[1])
|
||||||
|
matrices = fd_operators.deriv_back(dxes[1])
|
||||||
|
for axis in range(3):
|
||||||
|
shape = [1, 1, 1]
|
||||||
|
shape[axis] = len(coordinates[axis])
|
||||||
|
field = numpy.broadcast_to(coordinates[axis].reshape(shape), grid.shape)
|
||||||
|
interior = [slice(None)] * 3
|
||||||
|
# A linear coordinate is discontinuous at the periodic wrap.
|
||||||
|
interior[axis] = slice(None, -1) if forward else slice(1, None)
|
||||||
|
assert_close(functions[axis](field)[tuple(interior)], 1)
|
||||||
|
matrix_result = (matrices[axis] @ field.ravel()).reshape(grid.shape)
|
||||||
|
assert_close(matrix_result[tuple(interior)], 1)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue