[fdtd] reject zero and invalid CPML attenuation

Require a finite negative ln_R_per_layer before coefficient evaluation. Document None entries as the supported way to disable a face, and cover zero, signed zero, positive, and nonfinite targets with and without frequency shift.

Validation: 209 focused tests passed; Ruff passed.
This commit is contained in:
Forgejo Actions 2026-10-18 23:10:36 -07:00
commit 7b01ddd789
3 changed files with 17 additions and 1 deletions

View file

@ -18,3 +18,7 @@ CPML derivatives use electric widths for forward differences and magnetic
widths for backward differences. `cpml_params(..., dx=1.0)` specifies the uniform widths for backward differences. `cpml_params(..., dx=1.0)` specifies the uniform
physical cell width normal to a face; arbitrary nonuniform PML grading is not physical cell width normal to a face; arbitrary nonuniform PML grading is not
implemented by this constructor. Scale the timestep consistently with the grid. implemented by this constructor. Scale the timestep consistently with the grid.
`ln_R_per_layer` must be finite and strictly negative. Zero attenuation raises
`ValueError`; disable individual CPML faces with `None` in the parameter table
passed to `updates_with_cpml`.

View file

@ -52,7 +52,9 @@ def cpml_params(
`+1` for the high-index face). `+1` for the high-index face).
dt: Timestep used by the Yee update. dt: Timestep used by the Yee update.
thickness: Number of Yee cells occupied by the CPML region. thickness: Number of Yee cells occupied by the CPML region.
ln_R_per_layer: Logarithmic attenuation target per layer. ln_R_per_layer: Finite, strictly negative logarithmic attenuation target
per layer. Zero attenuation is not supported; disable a face with
``None`` in the updates_with_cpml parameter table instead.
epsilon_eff: Effective permittivity used when choosing the CPML scaling. epsilon_eff: Effective permittivity used when choosing the CPML scaling.
mu_eff: Effective permeability used when choosing the CPML scaling. mu_eff: Effective permeability used when choosing the CPML scaling.
m: Polynomial grading exponent for `sigma` and `kappa`. m: Polynomial grading exponent for `sigma` and `kappa`.
@ -82,6 +84,9 @@ def cpml_params(
if epsilon_eff <= 0: if epsilon_eff <= 0:
raise ValueError('epsilon_eff must be positive') raise ValueError('epsilon_eff must be positive')
if not numpy.isfinite(ln_R_per_layer) or ln_R_per_layer >= 0:
raise ValueError('ln_R_per_layer must be finite and negative; use None to disable a CPML face')
if not numpy.isfinite(dx) or dx <= 0: if not numpy.isfinite(dx) or dx <= 0:
raise ValueError('dx must be finite and positive') raise ValueError('dx must be finite and positive')

View file

@ -400,3 +400,10 @@ def test_scpml_builders_agree_including_real_profiles_and_touching_faces(shape,
for family in range(2): for family in range(2):
for axis in range(3): for axis in range(3):
assert_allclose(uniform[family][axis], explicit[family][axis], rtol=1e-14, atol=1e-14) assert_allclose(uniform[family][axis], explicit[family][axis], rtol=1e-14, atol=1e-14)
@pytest.mark.parametrize('attenuation', [0.0, -0.0, .1, np.inf, -np.inf, np.nan])
@pytest.mark.parametrize('alpha', [0, .2])
def test_cpml_rejects_zero_or_invalid_attenuation_before_coefficient_evaluation(attenuation, alpha):
with np.errstate(divide='raise', invalid='raise'), pytest.raises(ValueError, match='ln_R_per_layer must be finite and negative'):
pml.cpml_params(0, 1, .1, ln_R_per_layer=attenuation, cfs_alpha=alpha)