witwin-maxwell 0.3.0__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- witwin/maxwell/__init__.py +164 -0
- witwin/maxwell/adapters/__init__.py +1 -0
- witwin/maxwell/adapters/gds.py +203 -0
- witwin/maxwell/adapters/tidy3d.py +1669 -0
- witwin/maxwell/adjoint_inputs.py +70 -0
- witwin/maxwell/compiler/__init__.py +11 -0
- witwin/maxwell/compiler/materials.py +1355 -0
- witwin/maxwell/compiler/monitors.py +164 -0
- witwin/maxwell/compiler/sources.py +222 -0
- witwin/maxwell/fdfd/__init__.py +3 -0
- witwin/maxwell/fdfd/adjoint/__init__.py +3 -0
- witwin/maxwell/fdfd/adjoint/bridge.py +180 -0
- witwin/maxwell/fdfd/ams.py +300 -0
- witwin/maxwell/fdfd/krylov.py +280 -0
- witwin/maxwell/fdfd/plotting.py +110 -0
- witwin/maxwell/fdfd/postprocess.py +33 -0
- witwin/maxwell/fdfd/solver.py +1057 -0
- witwin/maxwell/fdtd/__init__.py +4 -0
- witwin/maxwell/fdtd/adjoint/__init__.py +21 -0
- witwin/maxwell/fdtd/adjoint/bridge.py +746 -0
- witwin/maxwell/fdtd/adjoint/capabilities.py +29 -0
- witwin/maxwell/fdtd/adjoint/core.py +3614 -0
- witwin/maxwell/fdtd/adjoint/dispatch.py +684 -0
- witwin/maxwell/fdtd/adjoint/native.py +2561 -0
- witwin/maxwell/fdtd/adjoint/profiler.py +195 -0
- witwin/maxwell/fdtd/adjoint/reverse_common.py +207 -0
- witwin/maxwell/fdtd/adjoint/seeds.py +524 -0
- witwin/maxwell/fdtd/boundary/__init__.py +37 -0
- witwin/maxwell/fdtd/boundary/bloch.py +90 -0
- witwin/maxwell/fdtd/boundary/common.py +41 -0
- witwin/maxwell/fdtd/boundary/cpml.py +527 -0
- witwin/maxwell/fdtd/boundary/runtime.py +209 -0
- witwin/maxwell/fdtd/checkpoint.py +227 -0
- witwin/maxwell/fdtd/coords.py +32 -0
- witwin/maxwell/fdtd/cuda/__init__.py +11 -0
- witwin/maxwell/fdtd/cuda/backend.py +3532 -0
- witwin/maxwell/fdtd/cuda/build.py +292 -0
- witwin/maxwell/fdtd/cuda/extension.cpp +2053 -0
- witwin/maxwell/fdtd/cuda/kernels/adjoint.cu +5290 -0
- witwin/maxwell/fdtd/cuda/kernels/aniso.cu +1704 -0
- witwin/maxwell/fdtd/cuda/kernels/boundary.cu +424 -0
- witwin/maxwell/fdtd/cuda/kernels/common.cu +74 -0
- witwin/maxwell/fdtd/cuda/kernels/common.cuh +82 -0
- witwin/maxwell/fdtd/cuda/kernels/dispersive.cu +782 -0
- witwin/maxwell/fdtd/cuda/kernels/electric.cu +3613 -0
- witwin/maxwell/fdtd/cuda/kernels/magnetic.cu +781 -0
- witwin/maxwell/fdtd/cuda/kernels/observers.cu +347 -0
- witwin/maxwell/fdtd/cuda/kernels/projection.cu +211 -0
- witwin/maxwell/fdtd/cuda/kernels/sources.cu +1482 -0
- witwin/maxwell/fdtd/cuda/kernels/spectral.cu +297 -0
- witwin/maxwell/fdtd/cuda/launch.h +23 -0
- witwin/maxwell/fdtd/cuda/prebuilt/witwin_maxwell_fdtd_cuda.pyd +0 -0
- witwin/maxwell/fdtd/cuda/runtime/__init__.py +6 -0
- witwin/maxwell/fdtd/cuda/runtime/dispatcher.py +6 -0
- witwin/maxwell/fdtd/cuda/runtime/graph.py +33 -0
- witwin/maxwell/fdtd/cuda/tensors.h +35 -0
- witwin/maxwell/fdtd/dispersion.py +48 -0
- witwin/maxwell/fdtd/evaluation.py +290 -0
- witwin/maxwell/fdtd/excitation/__init__.py +27 -0
- witwin/maxwell/fdtd/excitation/injection.py +1102 -0
- witwin/maxwell/fdtd/excitation/modes.py +1675 -0
- witwin/maxwell/fdtd/excitation/spatial.py +469 -0
- witwin/maxwell/fdtd/excitation/temporal.py +510 -0
- witwin/maxwell/fdtd/excitation/tfsf_apply.py +264 -0
- witwin/maxwell/fdtd/excitation/tfsf_common.py +421 -0
- witwin/maxwell/fdtd/excitation/tfsf_specs.py +490 -0
- witwin/maxwell/fdtd/excitation/tfsf_state.py +778 -0
- witwin/maxwell/fdtd/io.py +31 -0
- witwin/maxwell/fdtd/material_pullback.py +371 -0
- witwin/maxwell/fdtd/meshing/__init__.py +3 -0
- witwin/maxwell/fdtd/meshing/autogrid.py +335 -0
- witwin/maxwell/fdtd/observers.py +1453 -0
- witwin/maxwell/fdtd/plotting.py +121 -0
- witwin/maxwell/fdtd/postprocess.py +205 -0
- witwin/maxwell/fdtd/runtime/__init__.py +119 -0
- witwin/maxwell/fdtd/runtime/initialization.py +236 -0
- witwin/maxwell/fdtd/runtime/materials.py +1563 -0
- witwin/maxwell/fdtd/runtime/module_cache.py +31 -0
- witwin/maxwell/fdtd/runtime/spectral.py +581 -0
- witwin/maxwell/fdtd/runtime/stepping.py +1987 -0
- witwin/maxwell/fdtd/solver.py +502 -0
- witwin/maxwell/geometry/__init__.py +5 -0
- witwin/maxwell/geometry/polyslab.py +299 -0
- witwin/maxwell/materials.py +3 -0
- witwin/maxwell/media.py +1467 -0
- witwin/maxwell/monitors.py +743 -0
- witwin/maxwell/ports.py +113 -0
- witwin/maxwell/postprocess/RCS.py +139 -0
- witwin/maxwell/postprocess/__init__.py +42 -0
- witwin/maxwell/postprocess/diffraction.py +219 -0
- witwin/maxwell/postprocess/directivity.py +152 -0
- witwin/maxwell/postprocess/emission.py +67 -0
- witwin/maxwell/postprocess/modal.py +324 -0
- witwin/maxwell/postprocess/nfft.py +266 -0
- witwin/maxwell/postprocess/scattering_parameters.py +358 -0
- witwin/maxwell/postprocess/stratton_chu.py +1193 -0
- witwin/maxwell/result.py +967 -0
- witwin/maxwell/scene.py +1338 -0
- witwin/maxwell/simulation.py +659 -0
- witwin/maxwell/sources.py +917 -0
- witwin/maxwell/visualization/__init__.py +28 -0
- witwin/maxwell/visualization/interactive.py +484 -0
- witwin/maxwell/visualization/plots.py +568 -0
- witwin_maxwell-0.3.0.dist-info/METADATA +184 -0
- witwin_maxwell-0.3.0.dist-info/RECORD +107 -0
- witwin_maxwell-0.3.0.dist-info/WHEEL +4 -0
- witwin_maxwell-0.3.0.dist-info/licenses/LICENSE +13 -0
|
@@ -0,0 +1,1563 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import torch
|
|
5
|
+
|
|
6
|
+
from ...compiler.materials import (
|
|
7
|
+
evaluate_material_permeability,
|
|
8
|
+
evaluate_material_permittivity,
|
|
9
|
+
material_model_has_full_anisotropy,
|
|
10
|
+
material_model_has_modulation,
|
|
11
|
+
material_model_has_nonlinearity,
|
|
12
|
+
)
|
|
13
|
+
from ..boundary import BOUNDARY_PERIODIC, has_complex_fields
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _any_component_nonzero(components: dict[str, torch.Tensor]) -> bool:
|
|
17
|
+
return any(torch.any(components[axis] != 0).item() for axis in ("x", "y", "z"))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_materials(solver, scene):
|
|
21
|
+
material_model = scene.compile_materials()
|
|
22
|
+
# A Bloch-periodic run carries complex phase-shifted field state. The
|
|
23
|
+
# instantaneous |E|^2 nonlinearity, the additive off-diagonal anisotropic
|
|
24
|
+
# curl correction, and the per-step modulation factor are all implemented on
|
|
25
|
+
# the real-valued Yee update kernels, and their coefficients are undefined on
|
|
26
|
+
# a complex field (|E|^2 is not analytic, and the real additive/scaling
|
|
27
|
+
# corrections would break the Bloch phase). Each needs a complex-field kernel
|
|
28
|
+
# variant before it can compose with Bloch periodicity.
|
|
29
|
+
if scene.boundary.uses_kind("bloch") and material_model_has_nonlinearity(material_model):
|
|
30
|
+
raise NotImplementedError(
|
|
31
|
+
"FDTD nonlinear media require the real-valued update kernels: a Bloch-periodic run carries "
|
|
32
|
+
"complex phase-shifted fields, for which the instantaneous |E|^2 Kerr/chi2/TPA coefficient is "
|
|
33
|
+
"not defined. Use a real-field boundary (PML/PEC/PMC/periodic) with the nonlinearity."
|
|
34
|
+
)
|
|
35
|
+
if scene.boundary.uses_kind("bloch") and material_model_has_full_anisotropy(material_model):
|
|
36
|
+
raise NotImplementedError(
|
|
37
|
+
"FDTD full (off-diagonal) anisotropic media use a real additive curl(H) correction: a "
|
|
38
|
+
"Bloch-periodic run carries complex phase-shifted fields, and applying the real off-diagonal "
|
|
39
|
+
"correction to a complex field would break the Bloch phase relation. Use a real-field boundary "
|
|
40
|
+
"(PML/PEC/PMC/periodic) or an axis-aligned DiagonalTensor3."
|
|
41
|
+
)
|
|
42
|
+
if scene.boundary.uses_kind("bloch") and material_model_has_modulation(material_model):
|
|
43
|
+
raise NotImplementedError(
|
|
44
|
+
"FDTD time-modulated media scale the real-valued update coefficients by a per-step modulation "
|
|
45
|
+
"factor: a Bloch-periodic run carries complex phase-shifted fields, and the real scaling does "
|
|
46
|
+
"not track the complex Bloch phase. Use a real-field boundary (PML/PEC/PMC/periodic) with the "
|
|
47
|
+
"modulation."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
solver._compiled_material_model = material_model
|
|
51
|
+
eps_components = material_model["eps_components"]
|
|
52
|
+
mu_components = material_model["mu_components"]
|
|
53
|
+
solver.epsilon_r_components = eps_components
|
|
54
|
+
solver.mu_r_components = mu_components
|
|
55
|
+
solver.epsilon_r = material_model["eps_r"]
|
|
56
|
+
solver.mu_r = material_model["mu_r"]
|
|
57
|
+
solver.material_eps_r = evaluate_material_permittivity(material_model, solver.source_frequency)
|
|
58
|
+
solver.material_mu_r = evaluate_material_permeability(material_model, solver.source_frequency)
|
|
59
|
+
|
|
60
|
+
eps_x_node = eps_components["x"] * solver.eps0
|
|
61
|
+
eps_y_node = eps_components["y"] * solver.eps0
|
|
62
|
+
eps_z_node = eps_components["z"] * solver.eps0
|
|
63
|
+
mu_x_node = mu_components["x"] * solver.mu0
|
|
64
|
+
mu_y_node = mu_components["y"] * solver.mu0
|
|
65
|
+
mu_z_node = mu_components["z"] * solver.mu0
|
|
66
|
+
|
|
67
|
+
solver.eps_Ex = average_node_to_component(solver, eps_x_node, "Ex")
|
|
68
|
+
solver.eps_Ey = average_node_to_component(solver, eps_y_node, "Ey")
|
|
69
|
+
solver.eps_Ez = average_node_to_component(solver, eps_z_node, "Ez")
|
|
70
|
+
sigma_e_components = material_model["sigma_e_components"]
|
|
71
|
+
solver.sigma_e_Ex = average_node_to_component(solver, sigma_e_components["x"], "Ex")
|
|
72
|
+
solver.sigma_e_Ey = average_node_to_component(solver, sigma_e_components["y"], "Ey")
|
|
73
|
+
solver.sigma_e_Ez = average_node_to_component(solver, sigma_e_components["z"], "Ez")
|
|
74
|
+
solver.conductive_enabled = bool(_any_component_nonzero(sigma_e_components))
|
|
75
|
+
solver.mu_Hx = average_node_to_magnetic_component(solver, mu_x_node, "Hx")
|
|
76
|
+
solver.mu_Hy = average_node_to_magnetic_component(solver, mu_y_node, "Hy")
|
|
77
|
+
solver.mu_Hz = average_node_to_magnetic_component(solver, mu_z_node, "Hz")
|
|
78
|
+
# Static magnetic conductivity averaged onto the H components (same node->face
|
|
79
|
+
# stencil as mu): folds semi-implicitly into the H-update decay/curl exactly as
|
|
80
|
+
# sigma_e folds into the E-update, giving a magnetic conduction loss on Faraday's law.
|
|
81
|
+
sigma_m_components = material_model["sigma_m_components"]
|
|
82
|
+
solver.sigma_m_Hx = average_node_to_magnetic_component(solver, sigma_m_components["x"], "Hx")
|
|
83
|
+
solver.sigma_m_Hy = average_node_to_magnetic_component(solver, sigma_m_components["y"], "Hy")
|
|
84
|
+
solver.sigma_m_Hz = average_node_to_magnetic_component(solver, sigma_m_components["z"], "Hz")
|
|
85
|
+
solver.magnetically_conductive_enabled = bool(_any_component_nonzero(sigma_m_components))
|
|
86
|
+
|
|
87
|
+
build_nonlinear_channels(solver, material_model)
|
|
88
|
+
|
|
89
|
+
build_full_anisotropy(solver, material_model)
|
|
90
|
+
|
|
91
|
+
build_modulation(solver, material_model)
|
|
92
|
+
|
|
93
|
+
build_dispersive_templates(solver, material_model)
|
|
94
|
+
build_magnetic_dispersive_templates(solver, material_model)
|
|
95
|
+
solver.dispersive_enabled = solver.electric_dispersive_enabled or solver.magnetic_dispersive_enabled
|
|
96
|
+
# Full (off-diagonal) anisotropic permittivity now composes with dispersion:
|
|
97
|
+
# electric poles enter isotropically and the ADE polarization current is folded
|
|
98
|
+
# through the same per-edge inverse permittivity tensor as curl(H) (see
|
|
99
|
+
# _apply_aniso_dispersive_corrections); magnetic dispersion is an independent
|
|
100
|
+
# H-side channel and does not touch the electric tensor.
|
|
101
|
+
# Space-time modulation now composes with electric/magnetic dispersion and with
|
|
102
|
+
# the instantaneous nonlinear channels: the modulation scales the eps_inf
|
|
103
|
+
# background (the modulated E-update coefficients) while the ADE polarization
|
|
104
|
+
# current and the field-dependent nonlinear coefficients are folded through the
|
|
105
|
+
# same per-step 1/m_next factor, so a modulated dispersive/nonlinear cell stays
|
|
106
|
+
# mutually consistent (electro-optic-modulator physics). The full off-diagonal
|
|
107
|
+
# anisotropic tensor is the one edge left out: modulating the crystal-frame
|
|
108
|
+
# tensor would require a per-step re-inversion of the coupled 3x3 permittivity,
|
|
109
|
+
# which the current diagonalized effective-eps + additive off-diagonal split
|
|
110
|
+
# cannot express.
|
|
111
|
+
if solver.modulation_enabled and solver.full_aniso_enabled:
|
|
112
|
+
raise NotImplementedError(
|
|
113
|
+
"FDTD time-modulated media cannot be combined with full (off-diagonal) anisotropic "
|
|
114
|
+
"media: modulating the crystal-frame permittivity tensor needs a per-step re-inversion "
|
|
115
|
+
"of the coupled 3x3 tensor, which the diagonalized effective-permittivity update path "
|
|
116
|
+
"does not provide. Use a diagonal (axis-aligned) tensor or drop the modulation."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def build_nonlinear_channels(solver, material_model):
|
|
121
|
+
"""Set the nonlinear feature flags and edge-averaged nonlinear channel fields.
|
|
122
|
+
|
|
123
|
+
``kerr_enabled`` marks the instantaneous chi3 (Kerr) channel and keeps the
|
|
124
|
+
existing dynamic-curl fast path when it is the only nonlinearity.
|
|
125
|
+
``nonlinear_general_enabled`` switches to the general nonlinear coefficient
|
|
126
|
+
kernel, which additionally handles the chi2 channel and a field-dependent
|
|
127
|
+
conductivity and therefore rewrites both the decay and curl coefficients
|
|
128
|
+
every step.
|
|
129
|
+
"""
|
|
130
|
+
solver.kerr_enabled = bool(torch.any(material_model["kerr_chi3"] != 0).item())
|
|
131
|
+
solver.chi2_enabled = bool(torch.any(material_model["chi2"] != 0).item())
|
|
132
|
+
solver.tpa_enabled = bool(torch.any(material_model["tpa_sigma"] != 0).item())
|
|
133
|
+
solver.nonlinear_enabled = solver.kerr_enabled or solver.chi2_enabled or solver.tpa_enabled
|
|
134
|
+
solver.nonlinear_general_enabled = solver.chi2_enabled or solver.tpa_enabled
|
|
135
|
+
solver.kerr_chi3 = material_model["kerr_chi3"]
|
|
136
|
+
if solver.nonlinear_enabled:
|
|
137
|
+
solver.kerr_chi3_Ex = average_node_to_component(solver, solver.kerr_chi3, "Ex")
|
|
138
|
+
solver.kerr_chi3_Ey = average_node_to_component(solver, solver.kerr_chi3, "Ey")
|
|
139
|
+
solver.kerr_chi3_Ez = average_node_to_component(solver, solver.kerr_chi3, "Ez")
|
|
140
|
+
solver.cex_curl_dynamic = torch.empty_like(solver.eps_Ex)
|
|
141
|
+
solver.cey_curl_dynamic = torch.empty_like(solver.eps_Ey)
|
|
142
|
+
solver.cez_curl_dynamic = torch.empty_like(solver.eps_Ez)
|
|
143
|
+
else:
|
|
144
|
+
solver.kerr_chi3_Ex = None
|
|
145
|
+
solver.kerr_chi3_Ey = None
|
|
146
|
+
solver.kerr_chi3_Ez = None
|
|
147
|
+
solver.cex_curl_dynamic = None
|
|
148
|
+
solver.cey_curl_dynamic = None
|
|
149
|
+
solver.cez_curl_dynamic = None
|
|
150
|
+
if solver.nonlinear_general_enabled:
|
|
151
|
+
chi2 = material_model["chi2"]
|
|
152
|
+
solver.nonlinear_chi2_Ex = average_node_to_component(solver, chi2, "Ex")
|
|
153
|
+
solver.nonlinear_chi2_Ey = average_node_to_component(solver, chi2, "Ey")
|
|
154
|
+
solver.nonlinear_chi2_Ez = average_node_to_component(solver, chi2, "Ez")
|
|
155
|
+
# Field-dependent conductivity channel of the general nonlinear kernel:
|
|
156
|
+
# sigma_NL = tpa_sigma * |E|^2, from TwoPhotonAbsorption descriptors.
|
|
157
|
+
tpa_sigma = material_model["tpa_sigma"]
|
|
158
|
+
solver.tpa_sigma_Ex = average_node_to_component(solver, tpa_sigma, "Ex")
|
|
159
|
+
solver.tpa_sigma_Ey = average_node_to_component(solver, tpa_sigma, "Ey")
|
|
160
|
+
solver.tpa_sigma_Ez = average_node_to_component(solver, tpa_sigma, "Ez")
|
|
161
|
+
solver.cex_decay_dynamic = torch.empty_like(solver.eps_Ex)
|
|
162
|
+
solver.cey_decay_dynamic = torch.empty_like(solver.eps_Ey)
|
|
163
|
+
solver.cez_decay_dynamic = torch.empty_like(solver.eps_Ez)
|
|
164
|
+
else:
|
|
165
|
+
solver.nonlinear_chi2_Ex = None
|
|
166
|
+
solver.nonlinear_chi2_Ey = None
|
|
167
|
+
solver.nonlinear_chi2_Ez = None
|
|
168
|
+
solver.tpa_sigma_Ex = None
|
|
169
|
+
solver.tpa_sigma_Ey = None
|
|
170
|
+
solver.tpa_sigma_Ez = None
|
|
171
|
+
solver.cex_decay_dynamic = None
|
|
172
|
+
solver.cey_decay_dynamic = None
|
|
173
|
+
solver.cez_decay_dynamic = None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def build_modulation(solver, material_model):
|
|
177
|
+
"""Set the space-time modulation flag and its per-edge quadrature fields.
|
|
178
|
+
|
|
179
|
+
The compiled ``modulation_cos`` / ``modulation_sin`` node fields
|
|
180
|
+
(``amplitude*cos(phase)`` and ``amplitude*sin(phase)``) are averaged onto the
|
|
181
|
+
Yee E edges once here; the per-step modulation factor
|
|
182
|
+
``m(t) = 1 + cos_field*cos(Omega*t) - sin_field*sin(Omega*t)`` is evaluated
|
|
183
|
+
inside the modulated E-update kernels from these static fields and a persistent
|
|
184
|
+
two-element device clock, so no coefficient tensor or host phase is rebuilt per
|
|
185
|
+
step.
|
|
186
|
+
"""
|
|
187
|
+
solver.modulation_enabled = bool(material_model_has_modulation(material_model))
|
|
188
|
+
if not solver.modulation_enabled:
|
|
189
|
+
solver._modulation_time = None
|
|
190
|
+
solver.mod_cos_Ex = None
|
|
191
|
+
solver.mod_cos_Ey = None
|
|
192
|
+
solver.mod_cos_Ez = None
|
|
193
|
+
solver.mod_sin_Ex = None
|
|
194
|
+
solver.mod_sin_Ey = None
|
|
195
|
+
solver.mod_sin_Ez = None
|
|
196
|
+
solver.mod_omega_Ex = None
|
|
197
|
+
solver.mod_omega_Ey = None
|
|
198
|
+
solver.mod_omega_Ez = None
|
|
199
|
+
return
|
|
200
|
+
solver._modulation_time = torch.full(
|
|
201
|
+
(2,), -float(solver.dt), device=solver.device, dtype=solver.Ex.dtype
|
|
202
|
+
)
|
|
203
|
+
modulation_cos = material_model["modulation_cos"]
|
|
204
|
+
modulation_sin = material_model["modulation_sin"]
|
|
205
|
+
modulation_omega = material_model["modulation_omega"]
|
|
206
|
+
solver.mod_cos_Ex = average_node_to_component(solver, modulation_cos, "Ex")
|
|
207
|
+
solver.mod_cos_Ey = average_node_to_component(solver, modulation_cos, "Ey")
|
|
208
|
+
solver.mod_cos_Ez = average_node_to_component(solver, modulation_cos, "Ez")
|
|
209
|
+
solver.mod_sin_Ex = average_node_to_component(solver, modulation_sin, "Ex")
|
|
210
|
+
solver.mod_sin_Ey = average_node_to_component(solver, modulation_sin, "Ey")
|
|
211
|
+
solver.mod_sin_Ez = average_node_to_component(solver, modulation_sin, "Ez")
|
|
212
|
+
# Frequency is a per-cell label, not an amplitude: reduce it onto each Yee edge
|
|
213
|
+
# with a MAX over the averaging stencil so a modulated/unmodulated interface
|
|
214
|
+
# edge keeps the modulated cell's full angular frequency (its quadrature depth
|
|
215
|
+
# already fades through the arithmetic cos/sin blend), letting disjoint
|
|
216
|
+
# structures modulate at independent frequencies in one run.
|
|
217
|
+
solver.mod_omega_Ex = _max_node_to_electric_component(modulation_omega, "Ex")
|
|
218
|
+
solver.mod_omega_Ey = _max_node_to_electric_component(modulation_omega, "Ey")
|
|
219
|
+
solver.mod_omega_Ez = _max_node_to_electric_component(modulation_omega, "Ez")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
_ANISO_ROW_PAIRS = {
|
|
223
|
+
# Inverse-tensor row entries coupling each E component to the two off-axis
|
|
224
|
+
# curl(H) components, in (first, second) order matching the update
|
|
225
|
+
# E_i += dt/eps0 * (inv_ij * <curlH_j> + inv_ik * <curlH_k>).
|
|
226
|
+
"Ex": ("xy", "xz"),
|
|
227
|
+
"Ey": ("xy", "yz"),
|
|
228
|
+
"Ez": ("xz", "yz"),
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _symmetric3_inverse(a, b, c, d, e, f):
|
|
233
|
+
"""Closed-form inverse of the symmetric 3x3 tensor [[a,d,e],[d,b,f],[e,f,c]].
|
|
234
|
+
|
|
235
|
+
Returns the six independent entries of the (symmetric) inverse as a dict
|
|
236
|
+
keyed by ``xx, yy, zz, xy, xz, yz`` plus the determinant for validation.
|
|
237
|
+
"""
|
|
238
|
+
det = a * (b * c - f * f) - d * (d * c - f * e) + e * (d * f - b * e)
|
|
239
|
+
inv_det = 1.0 / det
|
|
240
|
+
return {
|
|
241
|
+
"xx": (b * c - f * f) * inv_det,
|
|
242
|
+
"yy": (a * c - e * e) * inv_det,
|
|
243
|
+
"zz": (a * b - d * d) * inv_det,
|
|
244
|
+
"xy": (e * f - d * c) * inv_det,
|
|
245
|
+
"xz": (d * f - b * e) * inv_det,
|
|
246
|
+
"yz": (d * e - a * f) * inv_det,
|
|
247
|
+
}, det
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def build_full_anisotropy(solver, material_model):
|
|
251
|
+
"""Precompute per-edge inverse-permittivity tensor rows for full anisotropy.
|
|
252
|
+
|
|
253
|
+
For each E component the six relative-permittivity node grids (diagonal +
|
|
254
|
+
off-diagonal) are averaged onto that component's Yee edges, the symmetric
|
|
255
|
+
3x3 tensor is inverted per edge in closed form, and the row belonging to the
|
|
256
|
+
component is stored: the diagonal entry as an effective scalar permittivity
|
|
257
|
+
(consumed by ``build_update_coefficients`` for the base curl coefficient)
|
|
258
|
+
and the two off-diagonal entries as coupling coefficients for the
|
|
259
|
+
neighbor-averaged off-axis curl(H) correction kernel.
|
|
260
|
+
"""
|
|
261
|
+
solver.full_aniso_enabled = bool(material_model_has_full_anisotropy(material_model))
|
|
262
|
+
solver._aniso_eps_eff = None
|
|
263
|
+
solver._aniso_inverse_rows = None
|
|
264
|
+
# Effective inverse permittivity (1/eps_eff) reused for the diagonal ADE
|
|
265
|
+
# polarization-current subtraction, and the accumulated per-component
|
|
266
|
+
# polarization current buffers used by the off-diagonal tensor subtraction.
|
|
267
|
+
solver._aniso_disp_inv_eps = None
|
|
268
|
+
solver._aniso_disp_current = None
|
|
269
|
+
# Per-component conduction-current buffers (sigma . E at the pre-update field),
|
|
270
|
+
# subtracted through the off-diagonal inverse tensor when the anisotropic
|
|
271
|
+
# medium is also conductive; None otherwise.
|
|
272
|
+
solver._aniso_cond_current = None
|
|
273
|
+
solver.cex_aniso_y = None
|
|
274
|
+
solver.cex_aniso_z = None
|
|
275
|
+
solver.cey_aniso_x = None
|
|
276
|
+
solver.cey_aniso_z = None
|
|
277
|
+
solver.cez_aniso_x = None
|
|
278
|
+
solver.cez_aniso_y = None
|
|
279
|
+
# Per-direction CPML psi accumulators for the off-diagonal coupling, allocated
|
|
280
|
+
# only when the anisotropic structure runs under a CPML absorber; kept as
|
|
281
|
+
# attributes so a graph capture snapshots them like the base CPML psi.
|
|
282
|
+
for component in ("ex", "ey", "ez"):
|
|
283
|
+
for axis in ("x", "y", "z"):
|
|
284
|
+
setattr(solver, f"psi_{component}_aniso_{axis}", None)
|
|
285
|
+
solver._full_aniso_cpml_overlap = False
|
|
286
|
+
if not solver.full_aniso_enabled:
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
if solver.nonlinear_enabled:
|
|
290
|
+
raise NotImplementedError(
|
|
291
|
+
"FDTD full (off-diagonal) anisotropic media cannot be combined with nonlinear media."
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
eps_components = material_model["eps_components"]
|
|
295
|
+
offdiag = material_model["eps_offdiag_components"]
|
|
296
|
+
sigma_components = material_model["sigma_e_components"]
|
|
297
|
+
node = {
|
|
298
|
+
"xx": eps_components["x"],
|
|
299
|
+
"yy": eps_components["y"],
|
|
300
|
+
"zz": eps_components["z"],
|
|
301
|
+
"xy": offdiag["xy"],
|
|
302
|
+
"xz": offdiag["xz"],
|
|
303
|
+
"yz": offdiag["yz"],
|
|
304
|
+
}
|
|
305
|
+
diagonal_key = {"Ex": "xx", "Ey": "yy", "Ez": "zz"}
|
|
306
|
+
# Semi-implicit conductive fold: invert (eps_inf + dt/2 * diag(sigma)) instead of
|
|
307
|
+
# eps_inf so the same per-edge inverse tensor B = dt * (...)^-1 that couples
|
|
308
|
+
# curl(H) also carries the exact tensor conduction damping. In relative units
|
|
309
|
+
# the diagonal shift is (dt / (2 eps0)) * sigma_abs; adding a nonnegative
|
|
310
|
+
# diagonal to an SPD tensor keeps it SPD, so the inverse stays well defined.
|
|
311
|
+
conductive = solver.conductive_enabled
|
|
312
|
+
conductive_shift = solver.dt / (2.0 * solver.eps0)
|
|
313
|
+
solver._aniso_eps_eff = {}
|
|
314
|
+
solver._aniso_inverse_rows = {}
|
|
315
|
+
for component_name in ("Ex", "Ey", "Ez"):
|
|
316
|
+
edge = {
|
|
317
|
+
key: average_node_to_component(solver, tensor, component_name)
|
|
318
|
+
for key, tensor in node.items()
|
|
319
|
+
}
|
|
320
|
+
if conductive:
|
|
321
|
+
for diag_key, axis in (("xx", "x"), ("yy", "y"), ("zz", "z")):
|
|
322
|
+
sigma_edge = average_node_to_component(solver, sigma_components[axis], component_name)
|
|
323
|
+
edge[diag_key] = edge[diag_key] + conductive_shift * sigma_edge
|
|
324
|
+
inverse, det = _symmetric3_inverse(
|
|
325
|
+
edge["xx"], edge["yy"], edge["zz"], edge["xy"], edge["xz"], edge["yz"]
|
|
326
|
+
)
|
|
327
|
+
diagonal_inverse = inverse[diagonal_key[component_name]]
|
|
328
|
+
if bool(torch.any(det <= 0).item()) or bool(torch.any(diagonal_inverse <= 0).item()):
|
|
329
|
+
raise ValueError(
|
|
330
|
+
"The blended anisotropic permittivity tensor is not positive-definite on the "
|
|
331
|
+
f"{component_name} Yee edges; check overlapping structures and material regions."
|
|
332
|
+
)
|
|
333
|
+
# Effective scalar permittivity reproducing the diagonal inverse entry:
|
|
334
|
+
# the base curl coefficient dt/eps_eff == dt * inv_diag / eps0 == B_ii, and
|
|
335
|
+
# (when conductive) the diagonal decay 1 - sigma * B_ii is folded in by
|
|
336
|
+
# _electric_update_coefficients through the aniso_shifted branch.
|
|
337
|
+
solver._aniso_eps_eff[component_name] = (solver.eps0 / diagonal_inverse).contiguous()
|
|
338
|
+
first_pair, second_pair = _ANISO_ROW_PAIRS[component_name]
|
|
339
|
+
solver._aniso_inverse_rows[component_name] = (
|
|
340
|
+
inverse[first_pair].contiguous(),
|
|
341
|
+
inverse[second_pair].contiguous(),
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
if conductive:
|
|
345
|
+
# Buffers for the pre-update conduction current sigma . E^n, subtracted
|
|
346
|
+
# through the off-diagonal inverse tensor each step (the diagonal part is
|
|
347
|
+
# already folded into the decay coefficient).
|
|
348
|
+
solver._aniso_cond_current = {
|
|
349
|
+
"Ex": torch.zeros_like(solver.Ex),
|
|
350
|
+
"Ey": torch.zeros_like(solver.Ey),
|
|
351
|
+
"Ez": torch.zeros_like(solver.Ez),
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def average_node_to_component(solver, node_tensor, component_name):
|
|
356
|
+
del solver
|
|
357
|
+
if component_name == "Ex":
|
|
358
|
+
return (0.5 * (node_tensor[:-1, :, :] + node_tensor[1:, :, :])).contiguous()
|
|
359
|
+
if component_name == "Ey":
|
|
360
|
+
return (0.5 * (node_tensor[:, :-1, :] + node_tensor[:, 1:, :])).contiguous()
|
|
361
|
+
if component_name == "Ez":
|
|
362
|
+
return (0.5 * (node_tensor[:, :, :-1] + node_tensor[:, :, 1:])).contiguous()
|
|
363
|
+
raise ValueError(f"Unsupported electric field component {component_name!r}.")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _max_node_to_electric_component(node_tensor, component_name):
|
|
367
|
+
"""Reduce a per-node field onto a Yee E edge with a MAX over the two-node
|
|
368
|
+
stencil that ``average_node_to_component`` averages. Used for the modulation
|
|
369
|
+
angular-frequency label, where the arithmetic mean would halve the frequency at
|
|
370
|
+
an interface edge; the max keeps the modulated cell's frequency there."""
|
|
371
|
+
if component_name == "Ex":
|
|
372
|
+
return torch.maximum(node_tensor[:-1, :, :], node_tensor[1:, :, :]).contiguous()
|
|
373
|
+
if component_name == "Ey":
|
|
374
|
+
return torch.maximum(node_tensor[:, :-1, :], node_tensor[:, 1:, :]).contiguous()
|
|
375
|
+
if component_name == "Ez":
|
|
376
|
+
return torch.maximum(node_tensor[:, :, :-1], node_tensor[:, :, 1:]).contiguous()
|
|
377
|
+
raise ValueError(f"Unsupported electric field component {component_name!r}.")
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def average_node_to_magnetic_component(solver, node_tensor, component_name):
|
|
381
|
+
del solver
|
|
382
|
+
if component_name == "Hx":
|
|
383
|
+
return (
|
|
384
|
+
0.25
|
|
385
|
+
* (
|
|
386
|
+
node_tensor[:, :-1, :-1]
|
|
387
|
+
+ node_tensor[:, :-1, 1:]
|
|
388
|
+
+ node_tensor[:, 1:, :-1]
|
|
389
|
+
+ node_tensor[:, 1:, 1:]
|
|
390
|
+
)
|
|
391
|
+
).contiguous()
|
|
392
|
+
if component_name == "Hy":
|
|
393
|
+
return (
|
|
394
|
+
0.25
|
|
395
|
+
* (
|
|
396
|
+
node_tensor[:-1, :, :-1]
|
|
397
|
+
+ node_tensor[:-1, :, 1:]
|
|
398
|
+
+ node_tensor[1:, :, :-1]
|
|
399
|
+
+ node_tensor[1:, :, 1:]
|
|
400
|
+
)
|
|
401
|
+
).contiguous()
|
|
402
|
+
if component_name == "Hz":
|
|
403
|
+
return (
|
|
404
|
+
0.25
|
|
405
|
+
* (
|
|
406
|
+
node_tensor[:-1, :-1, :]
|
|
407
|
+
+ node_tensor[:-1, 1:, :]
|
|
408
|
+
+ node_tensor[1:, :-1, :]
|
|
409
|
+
+ node_tensor[1:, 1:, :]
|
|
410
|
+
)
|
|
411
|
+
).contiguous()
|
|
412
|
+
raise ValueError(f"Unsupported magnetic field component {component_name!r}.")
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
_ELECTRIC_COMPONENT_AXIS = {"Ex": "x", "Ey": "y", "Ez": "z"}
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _electric_pole_components(entry):
|
|
419
|
+
"""The E components a pole entry drives (sheet poles restrict to tangential axes)."""
|
|
420
|
+
axes = entry.get("axes")
|
|
421
|
+
if axes is None:
|
|
422
|
+
return ("Ex", "Ey", "Ez")
|
|
423
|
+
return tuple(
|
|
424
|
+
component_name
|
|
425
|
+
for component_name, axis in _ELECTRIC_COMPONENT_AXIS.items()
|
|
426
|
+
if axis in axes
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def build_dispersive_templates(solver, material_model):
|
|
431
|
+
templates = {
|
|
432
|
+
"Ex": {"inv_eps": (1.0 / solver.eps_Ex).contiguous(), "debye": [], "drude": [], "lorentz": []},
|
|
433
|
+
"Ey": {"inv_eps": (1.0 / solver.eps_Ey).contiguous(), "debye": [], "drude": [], "lorentz": []},
|
|
434
|
+
"Ez": {"inv_eps": (1.0 / solver.eps_Ez).contiguous(), "debye": [], "drude": [], "lorentz": []},
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
for entry in material_model["debye_poles"]:
|
|
438
|
+
pole = entry["pole"]
|
|
439
|
+
decay = (2.0 * pole.tau - solver.dt) / (2.0 * pole.tau + solver.dt)
|
|
440
|
+
base_scale = 2.0 * solver.eps0 * pole.delta_eps * solver.dt / (2.0 * pole.tau + solver.dt)
|
|
441
|
+
for component_name in _electric_pole_components(entry):
|
|
442
|
+
weight = average_node_to_component(solver, entry["weight"], component_name)
|
|
443
|
+
templates[component_name]["debye"].append(
|
|
444
|
+
{
|
|
445
|
+
"decay": float(decay),
|
|
446
|
+
"drive": (weight * base_scale).contiguous(),
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
for entry in material_model["drude_poles"]:
|
|
451
|
+
pole = entry["pole"]
|
|
452
|
+
omega_p = 2.0 * np.pi * pole.plasma_frequency
|
|
453
|
+
gamma = 2.0 * np.pi * pole.gamma
|
|
454
|
+
denom = 2.0 + gamma * solver.dt
|
|
455
|
+
decay = (2.0 - gamma * solver.dt) / denom
|
|
456
|
+
base_scale = 2.0 * solver.eps0 * omega_p * omega_p * solver.dt / denom
|
|
457
|
+
for component_name in _electric_pole_components(entry):
|
|
458
|
+
weight = average_node_to_component(solver, entry["weight"], component_name)
|
|
459
|
+
templates[component_name]["drude"].append(
|
|
460
|
+
{
|
|
461
|
+
"decay": float(decay),
|
|
462
|
+
"drive": (weight * base_scale).contiguous(),
|
|
463
|
+
}
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
for entry in material_model["lorentz_poles"]:
|
|
467
|
+
pole = entry["pole"]
|
|
468
|
+
omega_0 = 2.0 * np.pi * pole.resonance_frequency
|
|
469
|
+
gamma = 2.0 * np.pi * pole.gamma
|
|
470
|
+
denom = 2.0 + gamma * solver.dt
|
|
471
|
+
decay = (2.0 - gamma * solver.dt) / denom
|
|
472
|
+
restoring = 2.0 * omega_0 * omega_0 * solver.dt / denom
|
|
473
|
+
base_scale = 2.0 * solver.eps0 * pole.delta_eps * omega_0 * omega_0 * solver.dt / denom
|
|
474
|
+
for component_name in _electric_pole_components(entry):
|
|
475
|
+
weight = average_node_to_component(solver, entry["weight"], component_name)
|
|
476
|
+
templates[component_name]["lorentz"].append(
|
|
477
|
+
{
|
|
478
|
+
"decay": float(decay),
|
|
479
|
+
"restoring": float(restoring),
|
|
480
|
+
"drive": (weight * base_scale).contiguous(),
|
|
481
|
+
}
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
solver._dispersive_templates = templates
|
|
485
|
+
solver.electric_dispersive_enabled = any(
|
|
486
|
+
templates[component_name][model_name]
|
|
487
|
+
for component_name in ("Ex", "Ey", "Ez")
|
|
488
|
+
for model_name in ("debye", "drude", "lorentz")
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def build_magnetic_dispersive_templates(solver, material_model):
|
|
493
|
+
templates = {
|
|
494
|
+
"Hx": {"inv_mu": (1.0 / solver.mu_Hx).contiguous(), "debye": [], "drude": [], "lorentz": []},
|
|
495
|
+
"Hy": {"inv_mu": (1.0 / solver.mu_Hy).contiguous(), "debye": [], "drude": [], "lorentz": []},
|
|
496
|
+
"Hz": {"inv_mu": (1.0 / solver.mu_Hz).contiguous(), "debye": [], "drude": [], "lorentz": []},
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
for entry in material_model["mu_debye_poles"]:
|
|
500
|
+
pole = entry["pole"]
|
|
501
|
+
decay = (2.0 * pole.tau - solver.dt) / (2.0 * pole.tau + solver.dt)
|
|
502
|
+
base_scale = 2.0 * solver.mu0 * pole.delta_eps * solver.dt / (2.0 * pole.tau + solver.dt)
|
|
503
|
+
for component_name in ("Hx", "Hy", "Hz"):
|
|
504
|
+
weight = average_node_to_magnetic_component(solver, entry["weight"], component_name)
|
|
505
|
+
templates[component_name]["debye"].append(
|
|
506
|
+
{
|
|
507
|
+
"decay": float(decay),
|
|
508
|
+
"drive": (weight * base_scale).contiguous(),
|
|
509
|
+
}
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
for entry in material_model["mu_drude_poles"]:
|
|
513
|
+
pole = entry["pole"]
|
|
514
|
+
omega_p = 2.0 * np.pi * pole.plasma_frequency
|
|
515
|
+
gamma = 2.0 * np.pi * pole.gamma
|
|
516
|
+
denom = 2.0 + gamma * solver.dt
|
|
517
|
+
decay = (2.0 - gamma * solver.dt) / denom
|
|
518
|
+
base_scale = 2.0 * solver.mu0 * omega_p * omega_p * solver.dt / denom
|
|
519
|
+
for component_name in ("Hx", "Hy", "Hz"):
|
|
520
|
+
weight = average_node_to_magnetic_component(solver, entry["weight"], component_name)
|
|
521
|
+
templates[component_name]["drude"].append(
|
|
522
|
+
{
|
|
523
|
+
"decay": float(decay),
|
|
524
|
+
"drive": (weight * base_scale).contiguous(),
|
|
525
|
+
}
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
for entry in material_model["mu_lorentz_poles"]:
|
|
529
|
+
pole = entry["pole"]
|
|
530
|
+
omega_0 = 2.0 * np.pi * pole.resonance_frequency
|
|
531
|
+
gamma = 2.0 * np.pi * pole.gamma
|
|
532
|
+
denom = 2.0 + gamma * solver.dt
|
|
533
|
+
decay = (2.0 - gamma * solver.dt) / denom
|
|
534
|
+
restoring = 2.0 * omega_0 * omega_0 * solver.dt / denom
|
|
535
|
+
base_scale = 2.0 * solver.mu0 * pole.delta_eps * omega_0 * omega_0 * solver.dt / denom
|
|
536
|
+
for component_name in ("Hx", "Hy", "Hz"):
|
|
537
|
+
weight = average_node_to_magnetic_component(solver, entry["weight"], component_name)
|
|
538
|
+
templates[component_name]["lorentz"].append(
|
|
539
|
+
{
|
|
540
|
+
"decay": float(decay),
|
|
541
|
+
"restoring": float(restoring),
|
|
542
|
+
"drive": (weight * base_scale).contiguous(),
|
|
543
|
+
}
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
solver._magnetic_dispersive_templates = templates
|
|
547
|
+
solver.magnetic_dispersive_enabled = any(
|
|
548
|
+
templates[component_name][model_name]
|
|
549
|
+
for component_name in ("Hx", "Hy", "Hz")
|
|
550
|
+
for model_name in ("debye", "drude", "lorentz")
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def initialize_dispersive_state(solver):
|
|
555
|
+
if not solver._dispersive_templates:
|
|
556
|
+
solver.electric_dispersive_enabled = False
|
|
557
|
+
return
|
|
558
|
+
|
|
559
|
+
if getattr(solver, "full_aniso_enabled", False) and solver.electric_dispersive_enabled:
|
|
560
|
+
# Accumulator for the total per-component polarization current, summed over
|
|
561
|
+
# all electric poles, consumed by the coupled tensor ADE subtraction.
|
|
562
|
+
solver._aniso_disp_current = {
|
|
563
|
+
"Ex": torch.zeros_like(solver.Ex),
|
|
564
|
+
"Ey": torch.zeros_like(solver.Ey),
|
|
565
|
+
"Ez": torch.zeros_like(solver.Ez),
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
for component_name, field in (("Ex", solver.Ex), ("Ey", solver.Ey), ("Ez", solver.Ez)):
|
|
569
|
+
component_templates = solver._dispersive_templates[component_name]
|
|
570
|
+
for entry in component_templates["debye"]:
|
|
571
|
+
entry["polarization"] = torch.zeros_like(field)
|
|
572
|
+
entry["current"] = torch.zeros_like(field)
|
|
573
|
+
entry["polarization_imag"] = torch.zeros_like(field) if has_complex_fields(solver) else None
|
|
574
|
+
entry["current_imag"] = torch.zeros_like(field) if has_complex_fields(solver) else None
|
|
575
|
+
for entry in component_templates["drude"]:
|
|
576
|
+
entry["current"] = torch.zeros_like(field)
|
|
577
|
+
entry["current_imag"] = torch.zeros_like(field) if has_complex_fields(solver) else None
|
|
578
|
+
for entry in component_templates["lorentz"]:
|
|
579
|
+
entry["polarization"] = torch.zeros_like(field)
|
|
580
|
+
entry["current"] = torch.zeros_like(field)
|
|
581
|
+
if has_complex_fields(solver):
|
|
582
|
+
entry["polarization_imag"] = torch.zeros_like(field)
|
|
583
|
+
entry["current_imag"] = torch.zeros_like(field)
|
|
584
|
+
else:
|
|
585
|
+
entry["polarization_imag"] = None
|
|
586
|
+
entry["current_imag"] = None
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def initialize_magnetic_dispersive_state(solver):
|
|
590
|
+
if not solver._magnetic_dispersive_templates:
|
|
591
|
+
solver.magnetic_dispersive_enabled = False
|
|
592
|
+
return
|
|
593
|
+
|
|
594
|
+
for component_name, field in (("Hx", solver.Hx), ("Hy", solver.Hy), ("Hz", solver.Hz)):
|
|
595
|
+
component_templates = solver._magnetic_dispersive_templates[component_name]
|
|
596
|
+
for entry in component_templates["debye"]:
|
|
597
|
+
entry["polarization"] = torch.zeros_like(field)
|
|
598
|
+
entry["current"] = torch.zeros_like(field)
|
|
599
|
+
entry["polarization_imag"] = torch.zeros_like(field) if has_complex_fields(solver) else None
|
|
600
|
+
entry["current_imag"] = torch.zeros_like(field) if has_complex_fields(solver) else None
|
|
601
|
+
for entry in component_templates["drude"]:
|
|
602
|
+
entry["current"] = torch.zeros_like(field)
|
|
603
|
+
entry["current_imag"] = torch.zeros_like(field) if has_complex_fields(solver) else None
|
|
604
|
+
for entry in component_templates["lorentz"]:
|
|
605
|
+
entry["polarization"] = torch.zeros_like(field)
|
|
606
|
+
entry["current"] = torch.zeros_like(field)
|
|
607
|
+
if has_complex_fields(solver):
|
|
608
|
+
entry["polarization_imag"] = torch.zeros_like(field)
|
|
609
|
+
entry["current_imag"] = torch.zeros_like(field)
|
|
610
|
+
else:
|
|
611
|
+
entry["polarization_imag"] = None
|
|
612
|
+
entry["current_imag"] = None
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def advance_component_dispersive_state(solver, component_name, field, *, imag=False):
|
|
616
|
+
if not solver.electric_dispersive_enabled:
|
|
617
|
+
return
|
|
618
|
+
|
|
619
|
+
component_templates = solver._dispersive_templates.get(component_name)
|
|
620
|
+
if not component_templates:
|
|
621
|
+
return
|
|
622
|
+
|
|
623
|
+
for entry in component_templates["debye"]:
|
|
624
|
+
polarization = entry["polarization_imag"] if imag else entry["polarization"]
|
|
625
|
+
current = entry["current_imag"] if imag else entry["current"]
|
|
626
|
+
solver.fdtd_module.updateDebyeCurrent3D(
|
|
627
|
+
ElectricField=field,
|
|
628
|
+
Polarization=polarization,
|
|
629
|
+
PolarizationCurrent=current,
|
|
630
|
+
DebyeDrive=entry["drive"],
|
|
631
|
+
decay=entry["decay"],
|
|
632
|
+
dt=solver.dt,
|
|
633
|
+
).launchRaw()
|
|
634
|
+
|
|
635
|
+
for entry in component_templates["drude"]:
|
|
636
|
+
current = entry["current_imag"] if imag else entry["current"]
|
|
637
|
+
solver.fdtd_module.updateDrudeCurrent3D(
|
|
638
|
+
ElectricField=field,
|
|
639
|
+
PolarizationCurrent=current,
|
|
640
|
+
DrudeDrive=entry["drive"],
|
|
641
|
+
decay=entry["decay"],
|
|
642
|
+
).launchRaw()
|
|
643
|
+
|
|
644
|
+
for entry in component_templates["lorentz"]:
|
|
645
|
+
polarization = entry["polarization_imag"] if imag else entry["polarization"]
|
|
646
|
+
current = entry["current_imag"] if imag else entry["current"]
|
|
647
|
+
solver.fdtd_module.updateLorentzCurrent3D(
|
|
648
|
+
ElectricField=field,
|
|
649
|
+
Polarization=polarization,
|
|
650
|
+
PolarizationCurrent=current,
|
|
651
|
+
LorentzDrive=entry["drive"],
|
|
652
|
+
decay=entry["decay"],
|
|
653
|
+
restoring=entry["restoring"],
|
|
654
|
+
dt=solver.dt,
|
|
655
|
+
).launchRaw()
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def advance_dispersive_state(solver):
|
|
659
|
+
if not solver.electric_dispersive_enabled:
|
|
660
|
+
return
|
|
661
|
+
|
|
662
|
+
advance_component_dispersive_state(solver, "Ex", solver.Ex, imag=False)
|
|
663
|
+
advance_component_dispersive_state(solver, "Ey", solver.Ey, imag=False)
|
|
664
|
+
advance_component_dispersive_state(solver, "Ez", solver.Ez, imag=False)
|
|
665
|
+
if has_complex_fields(solver):
|
|
666
|
+
advance_component_dispersive_state(solver, "Ex", solver.Ex_imag, imag=True)
|
|
667
|
+
advance_component_dispersive_state(solver, "Ey", solver.Ey_imag, imag=True)
|
|
668
|
+
advance_component_dispersive_state(solver, "Ez", solver.Ez_imag, imag=True)
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def advance_magnetic_component_dispersive_state(solver, component_name, field, *, imag=False):
|
|
672
|
+
if not solver.magnetic_dispersive_enabled:
|
|
673
|
+
return
|
|
674
|
+
|
|
675
|
+
component_templates = solver._magnetic_dispersive_templates.get(component_name)
|
|
676
|
+
if not component_templates:
|
|
677
|
+
return
|
|
678
|
+
|
|
679
|
+
for entry in component_templates["debye"]:
|
|
680
|
+
polarization = entry["polarization_imag"] if imag else entry["polarization"]
|
|
681
|
+
current = entry["current_imag"] if imag else entry["current"]
|
|
682
|
+
solver.fdtd_module.updateDebyeCurrent3D(
|
|
683
|
+
ElectricField=field,
|
|
684
|
+
Polarization=polarization,
|
|
685
|
+
PolarizationCurrent=current,
|
|
686
|
+
DebyeDrive=entry["drive"],
|
|
687
|
+
decay=entry["decay"],
|
|
688
|
+
dt=solver.dt,
|
|
689
|
+
).launchRaw()
|
|
690
|
+
|
|
691
|
+
for entry in component_templates["drude"]:
|
|
692
|
+
current = entry["current_imag"] if imag else entry["current"]
|
|
693
|
+
solver.fdtd_module.updateDrudeCurrent3D(
|
|
694
|
+
ElectricField=field,
|
|
695
|
+
PolarizationCurrent=current,
|
|
696
|
+
DrudeDrive=entry["drive"],
|
|
697
|
+
decay=entry["decay"],
|
|
698
|
+
).launchRaw()
|
|
699
|
+
|
|
700
|
+
for entry in component_templates["lorentz"]:
|
|
701
|
+
polarization = entry["polarization_imag"] if imag else entry["polarization"]
|
|
702
|
+
current = entry["current_imag"] if imag else entry["current"]
|
|
703
|
+
solver.fdtd_module.updateLorentzCurrent3D(
|
|
704
|
+
ElectricField=field,
|
|
705
|
+
Polarization=polarization,
|
|
706
|
+
PolarizationCurrent=current,
|
|
707
|
+
LorentzDrive=entry["drive"],
|
|
708
|
+
decay=entry["decay"],
|
|
709
|
+
restoring=entry["restoring"],
|
|
710
|
+
dt=solver.dt,
|
|
711
|
+
).launchRaw()
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def advance_magnetic_dispersive_state(solver):
|
|
715
|
+
if not solver.magnetic_dispersive_enabled:
|
|
716
|
+
return
|
|
717
|
+
|
|
718
|
+
advance_magnetic_component_dispersive_state(solver, "Hx", solver.Hx, imag=False)
|
|
719
|
+
advance_magnetic_component_dispersive_state(solver, "Hy", solver.Hy, imag=False)
|
|
720
|
+
advance_magnetic_component_dispersive_state(solver, "Hz", solver.Hz, imag=False)
|
|
721
|
+
if has_complex_fields(solver):
|
|
722
|
+
advance_magnetic_component_dispersive_state(solver, "Hx", solver.Hx_imag, imag=True)
|
|
723
|
+
advance_magnetic_component_dispersive_state(solver, "Hy", solver.Hy_imag, imag=True)
|
|
724
|
+
advance_magnetic_component_dispersive_state(solver, "Hz", solver.Hz_imag, imag=True)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def _dispersive_current_coefficient(solver, component_name, component_templates):
|
|
728
|
+
"""The ``(coefficient, dt)`` pair for the subtraction ``E -= dt * J_p * coefficient``.
|
|
729
|
+
|
|
730
|
+
In the linear path the polarization current is divided by the static
|
|
731
|
+
permittivity (``coefficient = 1/eps_lin``, ``dt = solver.dt``). When an
|
|
732
|
+
instantaneous nonlinearity is active the same Yee edge's displacement-current
|
|
733
|
+
term in the E update is scaled by the field-dependent ``c*_curl_dynamic``
|
|
734
|
+
coefficient (``external * (dt / eps_eff) / (1 + half)``); reusing that
|
|
735
|
+
coefficient here (with ``dt = 1``) subtracts the ADE current against the SAME
|
|
736
|
+
effective permittivity the ``curl(H)`` term saw this step, so the
|
|
737
|
+
instantaneous-nonlinear index shift and the dispersive response stay mutually
|
|
738
|
+
consistent (dividing the ADE current by ``eps_lin`` instead would detune the
|
|
739
|
+
dispersion by the nonlinear index shift). In a purely dispersive cell the
|
|
740
|
+
dynamic curl reduces to ``dt / eps_lin``, so the linear path is unchanged.
|
|
741
|
+
"""
|
|
742
|
+
if getattr(solver, "nonlinear_enabled", False):
|
|
743
|
+
dynamic_curl = {
|
|
744
|
+
"Ex": solver.cex_curl_dynamic,
|
|
745
|
+
"Ey": solver.cey_curl_dynamic,
|
|
746
|
+
"Ez": solver.cez_curl_dynamic,
|
|
747
|
+
}[component_name]
|
|
748
|
+
return dynamic_curl, 1.0
|
|
749
|
+
return component_templates["inv_eps"], solver.dt
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
_MODULATION_QUADRATURE_FIELDS = {
|
|
753
|
+
"Ex": ("mod_cos_Ex", "mod_sin_Ex", "mod_omega_Ex"),
|
|
754
|
+
"Ey": ("mod_cos_Ey", "mod_sin_Ey", "mod_omega_Ey"),
|
|
755
|
+
"Ez": ("mod_cos_Ez", "mod_sin_Ez", "mod_omega_Ez"),
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def apply_component_dispersive_currents(solver, component_name, field, *, imag=False):
|
|
760
|
+
if not solver.electric_dispersive_enabled:
|
|
761
|
+
return
|
|
762
|
+
|
|
763
|
+
component_templates = solver._dispersive_templates.get(component_name)
|
|
764
|
+
if not component_templates:
|
|
765
|
+
return
|
|
766
|
+
inv_eps, apply_dt = _dispersive_current_coefficient(solver, component_name, component_templates)
|
|
767
|
+
|
|
768
|
+
# When the electric dispersion co-exists with a space-time modulation, the ADE
|
|
769
|
+
# polarization current is divided by the same instantaneous eps_inf * m_next as
|
|
770
|
+
# the modulated curl(H) term (see apply_polarization_modulated_kernel); in a
|
|
771
|
+
# purely dispersive cell (m_next == 1) this reduces to the plain subtraction.
|
|
772
|
+
modulated = bool(getattr(solver, "modulation_enabled", False)) and not imag
|
|
773
|
+
if modulated:
|
|
774
|
+
cos_name, sin_name, omega_name = _MODULATION_QUADRATURE_FIELDS[component_name]
|
|
775
|
+
mod_cos = getattr(solver, cos_name)
|
|
776
|
+
mod_sin = getattr(solver, sin_name)
|
|
777
|
+
mod_omega = getattr(solver, omega_name)
|
|
778
|
+
|
|
779
|
+
def _subtract(current):
|
|
780
|
+
if modulated:
|
|
781
|
+
solver.fdtd_module.applyPolarizationCurrentModulated3D(
|
|
782
|
+
ElectricField=field,
|
|
783
|
+
PolarizationCurrent=current,
|
|
784
|
+
InvPermittivity=inv_eps,
|
|
785
|
+
ModCos=mod_cos,
|
|
786
|
+
ModSin=mod_sin,
|
|
787
|
+
ModOmega=mod_omega,
|
|
788
|
+
ModulationTime=solver._modulation_time,
|
|
789
|
+
dt=apply_dt,
|
|
790
|
+
).launchRaw()
|
|
791
|
+
else:
|
|
792
|
+
solver.fdtd_module.applyPolarizationCurrent3D(
|
|
793
|
+
ElectricField=field,
|
|
794
|
+
PolarizationCurrent=current,
|
|
795
|
+
InvPermittivity=inv_eps,
|
|
796
|
+
dt=apply_dt,
|
|
797
|
+
).launchRaw()
|
|
798
|
+
|
|
799
|
+
for model_name in ("debye", "drude", "lorentz"):
|
|
800
|
+
for entry in component_templates[model_name]:
|
|
801
|
+
_subtract(entry["current_imag"] if imag else entry["current"])
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def _aniso_periodic_flags(solver):
|
|
805
|
+
return (
|
|
806
|
+
int(solver.boundary_x_low_code == BOUNDARY_PERIODIC and solver.boundary_x_high_code == BOUNDARY_PERIODIC),
|
|
807
|
+
int(solver.boundary_y_low_code == BOUNDARY_PERIODIC and solver.boundary_y_high_code == BOUNDARY_PERIODIC),
|
|
808
|
+
int(solver.boundary_z_low_code == BOUNDARY_PERIODIC and solver.boundary_z_high_code == BOUNDARY_PERIODIC),
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def _apply_aniso_dispersive_corrections(solver):
|
|
813
|
+
"""Subtract the ADE polarization current through the full inverse permittivity tensor.
|
|
814
|
+
|
|
815
|
+
For a full (off-diagonal) anisotropic permittivity the instantaneous
|
|
816
|
+
constitutive relation is ``D = eps_inf . E + P``, so the Ampere update is
|
|
817
|
+
``E += dt * eps_inf^-1 . (curl H - J_p)``. The base and off-diagonal curl
|
|
818
|
+
kernels already apply ``eps_inf^-1`` (diagonal via ``dt/eps_eff`` and
|
|
819
|
+
off-diagonal via ``c*_aniso_*``) to ``curl H``; this applies the same operator
|
|
820
|
+
to the accumulated polarization current ``J_p``. Electric poles enter
|
|
821
|
+
isotropically (``P_i`` is driven by ``E_i``), so the per-component ADE state
|
|
822
|
+
advance is unchanged and only the subtraction becomes a tensor contraction.
|
|
823
|
+
Full anisotropy runs on real fields only (Bloch/complex is rejected upstream).
|
|
824
|
+
"""
|
|
825
|
+
templates = solver._dispersive_templates
|
|
826
|
+
totals = solver._aniso_disp_current
|
|
827
|
+
for component_name in ("Ex", "Ey", "Ez"):
|
|
828
|
+
buffer = totals[component_name]
|
|
829
|
+
buffer.zero_()
|
|
830
|
+
component_templates = templates[component_name]
|
|
831
|
+
for model_name in ("debye", "drude", "lorentz"):
|
|
832
|
+
for entry in component_templates[model_name]:
|
|
833
|
+
buffer.add_(entry["current"])
|
|
834
|
+
|
|
835
|
+
inv_eps = solver._aniso_disp_inv_eps
|
|
836
|
+
for component_name, field in (("Ex", solver.Ex), ("Ey", solver.Ey), ("Ez", solver.Ez)):
|
|
837
|
+
solver.fdtd_module.applyPolarizationCurrent3D(
|
|
838
|
+
ElectricField=field,
|
|
839
|
+
PolarizationCurrent=totals[component_name],
|
|
840
|
+
InvPermittivity=inv_eps[component_name],
|
|
841
|
+
dt=solver.dt,
|
|
842
|
+
).launchRaw()
|
|
843
|
+
|
|
844
|
+
periodic_x, periodic_y, periodic_z = _aniso_periodic_flags(solver)
|
|
845
|
+
solver.fdtd_module.applyAnisoOffdiagCurrentEx3D(
|
|
846
|
+
Ex=solver.Ex,
|
|
847
|
+
Jy=totals["Ey"],
|
|
848
|
+
Jz=totals["Ez"],
|
|
849
|
+
CoeffY=solver.cex_aniso_y,
|
|
850
|
+
CoeffZ=solver.cex_aniso_z,
|
|
851
|
+
periodicX=periodic_x,
|
|
852
|
+
periodicY=periodic_y,
|
|
853
|
+
periodicZ=periodic_z,
|
|
854
|
+
).launchRaw()
|
|
855
|
+
solver.fdtd_module.applyAnisoOffdiagCurrentEy3D(
|
|
856
|
+
Ey=solver.Ey,
|
|
857
|
+
Jx=totals["Ex"],
|
|
858
|
+
Jz=totals["Ez"],
|
|
859
|
+
CoeffX=solver.cey_aniso_x,
|
|
860
|
+
CoeffZ=solver.cey_aniso_z,
|
|
861
|
+
periodicX=periodic_x,
|
|
862
|
+
periodicY=periodic_y,
|
|
863
|
+
periodicZ=periodic_z,
|
|
864
|
+
).launchRaw()
|
|
865
|
+
solver.fdtd_module.applyAnisoOffdiagCurrentEz3D(
|
|
866
|
+
Ez=solver.Ez,
|
|
867
|
+
Jx=totals["Ex"],
|
|
868
|
+
Jy=totals["Ey"],
|
|
869
|
+
CoeffX=solver.cez_aniso_x,
|
|
870
|
+
CoeffY=solver.cez_aniso_y,
|
|
871
|
+
periodicX=periodic_x,
|
|
872
|
+
periodicY=periodic_y,
|
|
873
|
+
periodicZ=periodic_z,
|
|
874
|
+
).launchRaw()
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def apply_dispersive_corrections(solver):
|
|
878
|
+
if not solver.electric_dispersive_enabled:
|
|
879
|
+
return
|
|
880
|
+
|
|
881
|
+
if getattr(solver, "full_aniso_enabled", False):
|
|
882
|
+
_apply_aniso_dispersive_corrections(solver)
|
|
883
|
+
return
|
|
884
|
+
|
|
885
|
+
apply_component_dispersive_currents(solver, "Ex", solver.Ex, imag=False)
|
|
886
|
+
apply_component_dispersive_currents(solver, "Ey", solver.Ey, imag=False)
|
|
887
|
+
apply_component_dispersive_currents(solver, "Ez", solver.Ez, imag=False)
|
|
888
|
+
if has_complex_fields(solver):
|
|
889
|
+
apply_component_dispersive_currents(solver, "Ex", solver.Ex_imag, imag=True)
|
|
890
|
+
apply_component_dispersive_currents(solver, "Ey", solver.Ey_imag, imag=True)
|
|
891
|
+
apply_component_dispersive_currents(solver, "Ez", solver.Ez_imag, imag=True)
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def apply_magnetic_component_dispersive_currents(solver, component_name, field, *, imag=False):
|
|
895
|
+
if not solver.magnetic_dispersive_enabled:
|
|
896
|
+
return
|
|
897
|
+
|
|
898
|
+
component_templates = solver._magnetic_dispersive_templates.get(component_name)
|
|
899
|
+
if not component_templates:
|
|
900
|
+
return
|
|
901
|
+
inv_mu = component_templates["inv_mu"]
|
|
902
|
+
|
|
903
|
+
for entry in component_templates["debye"]:
|
|
904
|
+
current = entry["current_imag"] if imag else entry["current"]
|
|
905
|
+
solver.fdtd_module.applyPolarizationCurrent3D(
|
|
906
|
+
ElectricField=field,
|
|
907
|
+
PolarizationCurrent=current,
|
|
908
|
+
InvPermittivity=inv_mu,
|
|
909
|
+
dt=solver.dt,
|
|
910
|
+
).launchRaw()
|
|
911
|
+
|
|
912
|
+
for entry in component_templates["drude"]:
|
|
913
|
+
current = entry["current_imag"] if imag else entry["current"]
|
|
914
|
+
solver.fdtd_module.applyPolarizationCurrent3D(
|
|
915
|
+
ElectricField=field,
|
|
916
|
+
PolarizationCurrent=current,
|
|
917
|
+
InvPermittivity=inv_mu,
|
|
918
|
+
dt=solver.dt,
|
|
919
|
+
).launchRaw()
|
|
920
|
+
|
|
921
|
+
for entry in component_templates["lorentz"]:
|
|
922
|
+
current = entry["current_imag"] if imag else entry["current"]
|
|
923
|
+
solver.fdtd_module.applyPolarizationCurrent3D(
|
|
924
|
+
ElectricField=field,
|
|
925
|
+
PolarizationCurrent=current,
|
|
926
|
+
InvPermittivity=inv_mu,
|
|
927
|
+
dt=solver.dt,
|
|
928
|
+
).launchRaw()
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def apply_magnetic_dispersive_corrections(solver):
|
|
932
|
+
if not solver.magnetic_dispersive_enabled:
|
|
933
|
+
return
|
|
934
|
+
|
|
935
|
+
apply_magnetic_component_dispersive_currents(solver, "Hx", solver.Hx, imag=False)
|
|
936
|
+
apply_magnetic_component_dispersive_currents(solver, "Hy", solver.Hy, imag=False)
|
|
937
|
+
apply_magnetic_component_dispersive_currents(solver, "Hz", solver.Hz, imag=False)
|
|
938
|
+
if has_complex_fields(solver):
|
|
939
|
+
apply_magnetic_component_dispersive_currents(solver, "Hx", solver.Hx_imag, imag=True)
|
|
940
|
+
apply_magnetic_component_dispersive_currents(solver, "Hy", solver.Hy_imag, imag=True)
|
|
941
|
+
apply_magnetic_component_dispersive_currents(solver, "Hz", solver.Hz_imag, imag=True)
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def _electric_update_coefficients(solver, eps, sigma_e, pml_decay, *, aniso_shifted=False):
|
|
945
|
+
"""Compose the semi-implicit lossy-dielectric decay/curl factors for one E component.
|
|
946
|
+
|
|
947
|
+
``eps`` is the absolute permittivity (eps_r * eps0) and ``sigma_e`` the static
|
|
948
|
+
electric conductivity averaged onto the same Yee component. When conductivity is
|
|
949
|
+
inactive the material factors reduce to (decay=1, curl=dt/eps), so non-conductive
|
|
950
|
+
scenes keep the exact existing coefficients. ``pml_decay`` folds the CPML decay in
|
|
951
|
+
multiplicatively when present.
|
|
952
|
+
|
|
953
|
+
``aniso_shifted`` selects the full-tensor conductive path: ``eps`` is then the
|
|
954
|
+
effective permittivity ``eps0 / [(eps_inf + dt/2 diag(sigma))^-1]_ii`` whose
|
|
955
|
+
reciprocal is the diagonal of the conductively-shifted inverse tensor, so the
|
|
956
|
+
diagonal curl is already the semi-implicit ``B_ii = dt/eps`` and the decay is
|
|
957
|
+
``1 - sigma * B_ii`` (the same ``decay = 1 - sigma * curl`` identity the scalar
|
|
958
|
+
lossy fold satisfies). The off-diagonal conduction coupling is applied separately
|
|
959
|
+
through ``B_ij`` by the conduction-current subtraction.
|
|
960
|
+
"""
|
|
961
|
+
if solver.conductive_enabled:
|
|
962
|
+
if aniso_shifted:
|
|
963
|
+
curl = solver.dt / eps
|
|
964
|
+
material_decay = 1.0 - sigma_e * curl
|
|
965
|
+
else:
|
|
966
|
+
half = 0.5 * sigma_e * solver.dt / eps
|
|
967
|
+
denom = 1.0 + half
|
|
968
|
+
material_decay = (1.0 - half) / denom
|
|
969
|
+
curl = (solver.dt / eps) / denom
|
|
970
|
+
else:
|
|
971
|
+
material_decay = None
|
|
972
|
+
curl = solver.dt / eps
|
|
973
|
+
|
|
974
|
+
if pml_decay is None:
|
|
975
|
+
decay = material_decay if material_decay is not None else torch.ones_like(eps)
|
|
976
|
+
else:
|
|
977
|
+
decay = pml_decay if material_decay is None else material_decay * pml_decay
|
|
978
|
+
curl = curl * pml_decay
|
|
979
|
+
return decay.contiguous(), curl.contiguous()
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def _magnetic_update_coefficients(solver, mu, sigma_m, pml_decay):
|
|
983
|
+
"""Compose the semi-implicit magnetically-lossy decay/curl factors for one H component.
|
|
984
|
+
|
|
985
|
+
This is the exact magnetic dual of ``_electric_update_coefficients``: ``mu`` is
|
|
986
|
+
the absolute permeability (mu_r * mu0) and ``sigma_m`` the static magnetic
|
|
987
|
+
conductivity averaged onto the same Yee H component. A magnetic conduction
|
|
988
|
+
current ``sigma_m * H`` on Faraday's law, discretized semi-implicitly with H
|
|
989
|
+
averaged over the two half steps, gives the fold ``decay = (1 - h)/(1 + h)`` and
|
|
990
|
+
``curl = (dt/mu)/(1 + h)`` with ``h = 0.5 * sigma_m * dt / mu``. When magnetic
|
|
991
|
+
conductivity is inactive the factors reduce to ``(decay=1, curl=dt/mu)``, so a
|
|
992
|
+
non-magnetically-conductive scene keeps the exact existing coefficients.
|
|
993
|
+
``pml_decay`` folds the split-field CPML decay in multiplicatively when present,
|
|
994
|
+
matching the electric path (the per-cell decay tensor the H kernels already read).
|
|
995
|
+
"""
|
|
996
|
+
if solver.magnetically_conductive_enabled:
|
|
997
|
+
half = 0.5 * sigma_m * solver.dt / mu
|
|
998
|
+
denom = 1.0 + half
|
|
999
|
+
material_decay = (1.0 - half) / denom
|
|
1000
|
+
curl = (solver.dt / mu) / denom
|
|
1001
|
+
else:
|
|
1002
|
+
material_decay = None
|
|
1003
|
+
curl = solver.dt / mu
|
|
1004
|
+
|
|
1005
|
+
if pml_decay is None:
|
|
1006
|
+
decay = material_decay if material_decay is not None else torch.ones_like(mu)
|
|
1007
|
+
else:
|
|
1008
|
+
decay = pml_decay if material_decay is None else material_decay * pml_decay
|
|
1009
|
+
curl = curl * pml_decay
|
|
1010
|
+
return decay.contiguous(), curl.contiguous()
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
def _pec_edge_open_fractions(solver):
|
|
1014
|
+
"""Per-E-edge open fractions ``1 - fill`` from the PEC occupancy, or ``None``.
|
|
1015
|
+
|
|
1016
|
+
The PEC fill on each E edge is the two-endpoint node->edge average of the PEC
|
|
1017
|
+
occupancy (same stencil as eps, keeping it consistent and differentiable). In
|
|
1018
|
+
``staircase`` mode the fill is hard-thresholded at 0.5; in ``conformal`` mode the
|
|
1019
|
+
fractional fill is kept so the effective PEC wall sits at the sub-cell crossing.
|
|
1020
|
+
"""
|
|
1021
|
+
model = getattr(solver, "_compiled_material_model", None)
|
|
1022
|
+
pec_occupancy = None if model is None else model.get("pec_occupancy")
|
|
1023
|
+
if pec_occupancy is None:
|
|
1024
|
+
return None
|
|
1025
|
+
mode = model.get("pec_mode", "staircase")
|
|
1026
|
+
open_fractions = {}
|
|
1027
|
+
for component_name in ("Ex", "Ey", "Ez"):
|
|
1028
|
+
fill = average_node_to_component(solver, pec_occupancy, component_name)
|
|
1029
|
+
if mode == "staircase":
|
|
1030
|
+
fill = (fill >= 0.5).to(fill.dtype)
|
|
1031
|
+
open_fractions[component_name] = 1.0 - fill
|
|
1032
|
+
return open_fractions
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def _apply_pec_edge_suppression(solver):
|
|
1036
|
+
"""Fold the PEC open fraction into the electric decay/curl coefficients.
|
|
1037
|
+
|
|
1038
|
+
Scaling both ``decay`` and ``curl`` by ``open = 1 - fill`` turns the update into
|
|
1039
|
+
``E_new = open * (decay*E_old + curl*(curlH - J))``, so a fully covered edge
|
|
1040
|
+
(``fill = 1``) keeps tangential E exactly zero while a fractional edge acts as a
|
|
1041
|
+
soft short with sub-cell wall placement. Never amplifies (``open <= 1``), so the
|
|
1042
|
+
scheme is unconditionally stable and needs no area floor.
|
|
1043
|
+
"""
|
|
1044
|
+
open_fractions = _pec_edge_open_fractions(solver)
|
|
1045
|
+
if open_fractions is None:
|
|
1046
|
+
return
|
|
1047
|
+
solver.cex_decay = (solver.cex_decay * open_fractions["Ex"]).contiguous()
|
|
1048
|
+
solver.cex_curl = (solver.cex_curl * open_fractions["Ex"]).contiguous()
|
|
1049
|
+
solver.cey_decay = (solver.cey_decay * open_fractions["Ey"]).contiguous()
|
|
1050
|
+
solver.cey_curl = (solver.cey_curl * open_fractions["Ey"]).contiguous()
|
|
1051
|
+
solver.cez_decay = (solver.cez_decay * open_fractions["Ez"]).contiguous()
|
|
1052
|
+
solver.cez_curl = (solver.cez_curl * open_fractions["Ez"]).contiguous()
|
|
1053
|
+
if getattr(solver, "cex_decay_external", None) is not None:
|
|
1054
|
+
# The general nonlinear kernel recomposes decay/curl from this external
|
|
1055
|
+
# factor each step, so the PEC open fraction must fold into it too.
|
|
1056
|
+
solver.cex_decay_external = (solver.cex_decay_external * open_fractions["Ex"]).contiguous()
|
|
1057
|
+
solver.cey_decay_external = (solver.cey_decay_external * open_fractions["Ey"]).contiguous()
|
|
1058
|
+
solver.cez_decay_external = (solver.cez_decay_external * open_fractions["Ez"]).contiguous()
|
|
1059
|
+
if getattr(solver, "full_aniso_enabled", False):
|
|
1060
|
+
# The off-diagonal coupling terms are additive corrections to the same
|
|
1061
|
+
# E edges, so PEC-covered edges must suppress them identically.
|
|
1062
|
+
solver.cex_aniso_y = (solver.cex_aniso_y * open_fractions["Ex"]).contiguous()
|
|
1063
|
+
solver.cex_aniso_z = (solver.cex_aniso_z * open_fractions["Ex"]).contiguous()
|
|
1064
|
+
solver.cey_aniso_x = (solver.cey_aniso_x * open_fractions["Ey"]).contiguous()
|
|
1065
|
+
solver.cey_aniso_z = (solver.cey_aniso_z * open_fractions["Ey"]).contiguous()
|
|
1066
|
+
solver.cez_aniso_x = (solver.cez_aniso_x * open_fractions["Ez"]).contiguous()
|
|
1067
|
+
solver.cez_aniso_y = (solver.cez_aniso_y * open_fractions["Ez"]).contiguous()
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
_E_COMPONENTS = ("Ex", "Ey", "Ez")
|
|
1071
|
+
_H_COMPONENTS = ("Hx", "Hy", "Hz")
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
def _axis_index_tuple(ndim, axis, index_or_slice):
|
|
1075
|
+
selector = [slice(None)] * ndim
|
|
1076
|
+
selector[axis] = index_or_slice
|
|
1077
|
+
return tuple(selector)
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
def _configure_sibc(solver):
|
|
1081
|
+
"""Set up the surface-impedance (Leontovich) boundary from the compiled descriptor.
|
|
1082
|
+
|
|
1083
|
+
The good-conductor SIBC is realized by (a) masking the metal interior so its
|
|
1084
|
+
tangential E edges stay zero (a zero open-fraction folded into the update
|
|
1085
|
+
coefficients, exactly as PEC does) and (b) overriding the two tangential E
|
|
1086
|
+
faces on the surface node plane each step from the vacuum-side tangential H,
|
|
1087
|
+
which ``apply_sibc_surface`` in ``stepping.py`` performs. The surface
|
|
1088
|
+
impedance is evaluated at the operating frequency as a narrowband series R-L,
|
|
1089
|
+
``Zs(omega0) = R + j*omega0*Ls`` with ``R = sqrt(omega0*mu0/(2*sigma))`` and
|
|
1090
|
+
``Ls = R/omega0``, so ``Zs(omega0)`` reproduces the exact Leontovich value at
|
|
1091
|
+
the source frequency.
|
|
1092
|
+
"""
|
|
1093
|
+
solver.sibc_enabled = False
|
|
1094
|
+
solver._sibc = None
|
|
1095
|
+
model = getattr(solver, "_compiled_material_model", None)
|
|
1096
|
+
descriptor = None if model is None else model.get("sibc")
|
|
1097
|
+
if descriptor is None:
|
|
1098
|
+
return
|
|
1099
|
+
axis = int(descriptor["axis"])
|
|
1100
|
+
metal_side = descriptor["metal_side"]
|
|
1101
|
+
surface_node = int(descriptor["surface_node"])
|
|
1102
|
+
sigma = float(descriptor["conductivity"])
|
|
1103
|
+
omega0 = 2.0 * np.pi * float(solver.source_frequency)
|
|
1104
|
+
if omega0 <= 0.0:
|
|
1105
|
+
raise ValueError("LossyMetalMedium SIBC requires a positive operating frequency.")
|
|
1106
|
+
surface_r = float(np.sqrt(omega0 * solver.mu0 / (2.0 * sigma)))
|
|
1107
|
+
surface_l = surface_r / omega0
|
|
1108
|
+
|
|
1109
|
+
# Mask the metal interior: zero the update coefficients of the tangential
|
|
1110
|
+
# E edges strictly inside the metal (node index on the metal side of the
|
|
1111
|
+
# surface) and the normal E edges from the surface inward, so those edges
|
|
1112
|
+
# stay at their zero initial value and the surface behaves as a termination.
|
|
1113
|
+
if metal_side == "high":
|
|
1114
|
+
tangential_interior = slice(surface_node + 1, None)
|
|
1115
|
+
normal_interior = slice(surface_node, None)
|
|
1116
|
+
vacuum_h_index = surface_node - 1
|
|
1117
|
+
else:
|
|
1118
|
+
tangential_interior = slice(None, surface_node)
|
|
1119
|
+
normal_interior = slice(None, surface_node)
|
|
1120
|
+
vacuum_h_index = surface_node
|
|
1121
|
+
for component in _E_COMPONENTS:
|
|
1122
|
+
component_axis = _E_COMPONENTS.index(component)
|
|
1123
|
+
interior = normal_interior if component_axis == axis else tangential_interior
|
|
1124
|
+
decay = getattr(solver, f"c{component.lower()}_decay", None)
|
|
1125
|
+
if decay is None:
|
|
1126
|
+
continue
|
|
1127
|
+
# Open-fraction mask (1 outside the metal, 0 inside), multiplied into the
|
|
1128
|
+
# decay and curl coefficients out-of-place, mirroring the PEC suppression so
|
|
1129
|
+
# a covered edge keeps E exactly zero without perturbing autograd.
|
|
1130
|
+
mask = torch.ones_like(decay)
|
|
1131
|
+
mask[_axis_index_tuple(mask.dim(), axis, interior)] = 0.0
|
|
1132
|
+
for suffix in ("decay", "curl"):
|
|
1133
|
+
name = f"c{component.lower()}_{suffix}"
|
|
1134
|
+
setattr(solver, name, (getattr(solver, name) * mask).contiguous())
|
|
1135
|
+
|
|
1136
|
+
# Tangential components (b, c) in cyclic order after the normal axis, each
|
|
1137
|
+
# paired with the transverse H one cell out on the vacuum side. The Leontovich
|
|
1138
|
+
# relation E_t = Zs * (n_hat x H) with the metal outward normal n_hat gives
|
|
1139
|
+
# E_b = +sn*(R*H_c + Ls*dH_c/dt), E_c = -sn*(R*H_b + Ls*dH_b/dt); sn = +1 when
|
|
1140
|
+
# the metal fills the high side of the surface (n_hat = -axis) and -1 for the
|
|
1141
|
+
# low side. This is the passive (energy-absorbing) branch: the opposite sign is
|
|
1142
|
+
# a negative surface resistance and grows without bound.
|
|
1143
|
+
b = (axis + 1) % 3
|
|
1144
|
+
c = (axis + 2) % 3
|
|
1145
|
+
sn = 1.0 if metal_side == "high" else -1.0
|
|
1146
|
+
faces = (
|
|
1147
|
+
(_E_COMPONENTS[b], _H_COMPONENTS[c], +sn),
|
|
1148
|
+
(_E_COMPONENTS[c], _H_COMPONENTS[b], -sn),
|
|
1149
|
+
)
|
|
1150
|
+
face_state = []
|
|
1151
|
+
for e_name, h_name, sign in faces:
|
|
1152
|
+
h_tensor = getattr(solver, h_name)
|
|
1153
|
+
h_selector = _axis_index_tuple(h_tensor.dim(), axis, vacuum_h_index)
|
|
1154
|
+
e_selector = _axis_index_tuple(getattr(solver, e_name).dim(), axis, surface_node)
|
|
1155
|
+
face_state.append(
|
|
1156
|
+
{
|
|
1157
|
+
"e_name": e_name,
|
|
1158
|
+
"h_name": h_name,
|
|
1159
|
+
"sign": float(sign),
|
|
1160
|
+
"axis": axis,
|
|
1161
|
+
"electric_index": surface_node,
|
|
1162
|
+
"magnetic_index": vacuum_h_index,
|
|
1163
|
+
"e_selector": e_selector,
|
|
1164
|
+
"h_selector": h_selector,
|
|
1165
|
+
"h_prev": torch.zeros_like(h_tensor[h_selector]),
|
|
1166
|
+
}
|
|
1167
|
+
)
|
|
1168
|
+
solver._sibc = {
|
|
1169
|
+
"surface_r": surface_r,
|
|
1170
|
+
"surface_l": surface_l,
|
|
1171
|
+
"faces": tuple(face_state),
|
|
1172
|
+
}
|
|
1173
|
+
solver.sibc_enabled = True
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
def _store_nonlinear_external_decay(solver, ex_decay, ey_decay, ez_decay):
|
|
1177
|
+
"""Keep the material-independent (PML) decay factors for the nonlinear kernel.
|
|
1178
|
+
|
|
1179
|
+
The general nonlinear coefficient kernel recomposes the semi-implicit lossy
|
|
1180
|
+
decay/curl factors from the linear permittivity, the static conductivity, and
|
|
1181
|
+
the field-dependent nonlinear terms every step, so it needs the external
|
|
1182
|
+
(PML split-field) decay separately from the static material decay that
|
|
1183
|
+
``_electric_update_coefficients`` folds into ``c*_decay``. The PEC open
|
|
1184
|
+
fractions are folded in afterwards by ``_apply_pec_edge_suppression``.
|
|
1185
|
+
"""
|
|
1186
|
+
if not getattr(solver, "nonlinear_general_enabled", False):
|
|
1187
|
+
solver.cex_decay_external = None
|
|
1188
|
+
solver.cey_decay_external = None
|
|
1189
|
+
solver.cez_decay_external = None
|
|
1190
|
+
return
|
|
1191
|
+
solver.cex_decay_external = (
|
|
1192
|
+
torch.ones_like(solver.eps_Ex) if ex_decay is None else ex_decay.clone()
|
|
1193
|
+
).contiguous()
|
|
1194
|
+
solver.cey_decay_external = (
|
|
1195
|
+
torch.ones_like(solver.eps_Ey) if ey_decay is None else ey_decay.clone()
|
|
1196
|
+
).contiguous()
|
|
1197
|
+
solver.cez_decay_external = (
|
|
1198
|
+
torch.ones_like(solver.eps_Ez) if ez_decay is None else ez_decay.clone()
|
|
1199
|
+
).contiguous()
|
|
1200
|
+
|
|
1201
|
+
|
|
1202
|
+
def _electric_epsilon_tensors(solver):
|
|
1203
|
+
"""The absolute permittivity tensors feeding the electric curl coefficients.
|
|
1204
|
+
|
|
1205
|
+
Full anisotropy swaps in the effective scalar permittivity that reproduces
|
|
1206
|
+
the diagonal entry of the per-edge inverse tensor; the plain per-axis
|
|
1207
|
+
average is kept otherwise (byte-identical to the base path).
|
|
1208
|
+
"""
|
|
1209
|
+
if getattr(solver, "full_aniso_enabled", False):
|
|
1210
|
+
eps_eff = solver._aniso_eps_eff
|
|
1211
|
+
return eps_eff["Ex"], eps_eff["Ey"], eps_eff["Ez"]
|
|
1212
|
+
return solver.eps_Ex, solver.eps_Ey, solver.eps_Ez
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
def _build_full_aniso_curl_coefficients(solver):
|
|
1216
|
+
"""Materialize the off-diagonal coupling coefficient tensors.
|
|
1217
|
+
|
|
1218
|
+
``coeff = dt * inv_ij / eps0`` multiplies the neighbor-averaged off-axis
|
|
1219
|
+
curl(H) component in the correction kernel. The coefficients are zero
|
|
1220
|
+
outside the anisotropic structures, so the correction is a no-op there.
|
|
1221
|
+
"""
|
|
1222
|
+
if not getattr(solver, "full_aniso_enabled", False):
|
|
1223
|
+
return
|
|
1224
|
+
scale = solver.dt / solver.eps0
|
|
1225
|
+
rows = solver._aniso_inverse_rows
|
|
1226
|
+
solver.cex_aniso_y = (scale * rows["Ex"][0]).contiguous()
|
|
1227
|
+
solver.cex_aniso_z = (scale * rows["Ex"][1]).contiguous()
|
|
1228
|
+
solver.cey_aniso_x = (scale * rows["Ey"][0]).contiguous()
|
|
1229
|
+
solver.cey_aniso_z = (scale * rows["Ey"][1]).contiguous()
|
|
1230
|
+
solver.cez_aniso_x = (scale * rows["Ez"][0]).contiguous()
|
|
1231
|
+
solver.cez_aniso_y = (scale * rows["Ez"][1]).contiguous()
|
|
1232
|
+
# The diagonal ADE polarization-current subtraction must divide by the same
|
|
1233
|
+
# effective permittivity the base curl coefficient uses (dt/eps_eff), not the
|
|
1234
|
+
# plain per-axis average, so the dispersive response and the tensor curl stay
|
|
1235
|
+
# consistent on off-diagonal edges (they coincide for a diagonal tensor).
|
|
1236
|
+
if getattr(solver, "electric_dispersive_enabled", False):
|
|
1237
|
+
eps_eff = solver._aniso_eps_eff
|
|
1238
|
+
solver._aniso_disp_inv_eps = {
|
|
1239
|
+
component_name: (1.0 / eps_eff[component_name]).contiguous()
|
|
1240
|
+
for component_name in ("Ex", "Ey", "Ez")
|
|
1241
|
+
}
|
|
1242
|
+
_handle_full_aniso_absorber_overlap(solver)
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
def _absorbing_region_node_mask(solver):
|
|
1246
|
+
"""Boolean node mask of the absorbing-boundary region, or ``None``.
|
|
1247
|
+
|
|
1248
|
+
Covers both the graded-sigma split-field absorbers (3D ``sigma_*`` node
|
|
1249
|
+
profiles) and CPML/stable-PML (1D stretching profiles per axis).
|
|
1250
|
+
"""
|
|
1251
|
+
if solver.active_absorber_type in ("pml", "absorber"):
|
|
1252
|
+
return (solver.sigma_x != 0) | (solver.sigma_y != 0) | (solver.sigma_z != 0)
|
|
1253
|
+
if getattr(solver, "uses_cpml", False):
|
|
1254
|
+
masks = []
|
|
1255
|
+
for axis_name, view in (("x", (-1, 1, 1)), ("y", (1, -1, 1)), ("z", (1, 1, -1))):
|
|
1256
|
+
c_e = getattr(solver, f"cpml_c_e_{axis_name}", None)
|
|
1257
|
+
inv_kappa = getattr(solver, f"cpml_inv_kappa_e_{axis_name}", None)
|
|
1258
|
+
axis_mask = None
|
|
1259
|
+
if c_e is not None:
|
|
1260
|
+
axis_mask = c_e != 0
|
|
1261
|
+
if inv_kappa is not None:
|
|
1262
|
+
kappa_mask = inv_kappa != 1
|
|
1263
|
+
axis_mask = kappa_mask if axis_mask is None else (axis_mask | kappa_mask)
|
|
1264
|
+
if axis_mask is not None:
|
|
1265
|
+
masks.append(axis_mask.reshape(view))
|
|
1266
|
+
if not masks:
|
|
1267
|
+
return None
|
|
1268
|
+
mask = masks[0]
|
|
1269
|
+
for axis_mask in masks[1:]:
|
|
1270
|
+
mask = mask | axis_mask
|
|
1271
|
+
return mask.expand(solver.Nx, solver.Ny, solver.Nz)
|
|
1272
|
+
return None
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
def _full_aniso_offdiag_overlaps_absorber(solver):
|
|
1276
|
+
"""Whether any off-diagonal coupling coefficient reaches into the absorber."""
|
|
1277
|
+
mask = _absorbing_region_node_mask(solver)
|
|
1278
|
+
if mask is None:
|
|
1279
|
+
return False
|
|
1280
|
+
mask = mask.to(dtype=torch.float32)
|
|
1281
|
+
component_coefficients = (
|
|
1282
|
+
("Ex", (solver.cex_aniso_y, solver.cex_aniso_z)),
|
|
1283
|
+
("Ey", (solver.cey_aniso_x, solver.cey_aniso_z)),
|
|
1284
|
+
("Ez", (solver.cez_aniso_x, solver.cez_aniso_y)),
|
|
1285
|
+
)
|
|
1286
|
+
# The soft SDF occupancy leaves a vanishing (denormal-scale) tail far outside
|
|
1287
|
+
# the structure, so compare against a relative threshold instead of != 0.
|
|
1288
|
+
peak = max(
|
|
1289
|
+
float(coefficient.abs().max())
|
|
1290
|
+
for _, coefficients in component_coefficients
|
|
1291
|
+
for coefficient in coefficients
|
|
1292
|
+
)
|
|
1293
|
+
if peak == 0.0:
|
|
1294
|
+
return False
|
|
1295
|
+
threshold = 1.0e-6 * peak
|
|
1296
|
+
for component_name, coefficients in component_coefficients:
|
|
1297
|
+
edge_mask = average_node_to_component(solver, mask, component_name) > 0
|
|
1298
|
+
for coefficient in coefficients:
|
|
1299
|
+
if bool(torch.any(edge_mask & (coefficient.abs() > threshold)).item()):
|
|
1300
|
+
return True
|
|
1301
|
+
return False
|
|
1302
|
+
|
|
1303
|
+
|
|
1304
|
+
def _initialize_full_aniso_cpml_psi(solver):
|
|
1305
|
+
"""Allocate the per-direction off-diagonal CPML psi accumulators.
|
|
1306
|
+
|
|
1307
|
+
The CPML off-diagonal correction kernel coordinate-stretches each of the
|
|
1308
|
+
three spatial derivative directions with an independent recursive-convolution
|
|
1309
|
+
memory owned by the target E edge. The buffers share the E-field shapes and
|
|
1310
|
+
start at zero, so a structure that never reaches the absorber leaves them at
|
|
1311
|
+
zero and the correction is byte-consistent with the raw (no-stretch) update.
|
|
1312
|
+
"""
|
|
1313
|
+
for field_name, component in (("Ex", "ex"), ("Ey", "ey"), ("Ez", "ez")):
|
|
1314
|
+
field = getattr(solver, field_name)
|
|
1315
|
+
for axis in ("x", "y", "z"):
|
|
1316
|
+
setattr(solver, f"psi_{component}_aniso_{axis}", torch.zeros_like(field))
|
|
1317
|
+
|
|
1318
|
+
|
|
1319
|
+
def _handle_full_aniso_absorber_overlap(solver):
|
|
1320
|
+
"""Route an anisotropic structure that overlaps the absorbing boundary.
|
|
1321
|
+
|
|
1322
|
+
Under CPML the dedicated off-diagonal kernel coordinate-stretches the tensor
|
|
1323
|
+
coupling with its own psi memory (allocated here), so the overlap is
|
|
1324
|
+
supported; the overlap is recorded because the reverse adjoint replay does
|
|
1325
|
+
not yet stretch the off-diagonal, and the split-field graded-sigma absorbers
|
|
1326
|
+
('pml'/'absorber') carry no per-direction auxiliary memory to stretch the
|
|
1327
|
+
coupling, so an overlap there is rejected with a physical reason.
|
|
1328
|
+
"""
|
|
1329
|
+
solver._full_aniso_cpml_overlap = False
|
|
1330
|
+
if getattr(solver, "uses_cpml", False):
|
|
1331
|
+
_initialize_full_aniso_cpml_psi(solver)
|
|
1332
|
+
solver._full_aniso_cpml_overlap = _full_aniso_offdiag_overlaps_absorber(solver)
|
|
1333
|
+
return
|
|
1334
|
+
if _full_aniso_offdiag_overlaps_absorber(solver):
|
|
1335
|
+
raise NotImplementedError(
|
|
1336
|
+
"FDTD full (off-diagonal) anisotropic media overlapping a split-field "
|
|
1337
|
+
"PML/absorber layer are not supported: the graded-sigma absorber carries no "
|
|
1338
|
+
"per-direction auxiliary memory to coordinate-stretch the off-diagonal tensor "
|
|
1339
|
+
"coupling. Use the default CPML absorber for anisotropic structures that reach "
|
|
1340
|
+
"the boundary region."
|
|
1341
|
+
)
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
def _uniform_coefficient_value(tensor: torch.Tensor) -> float | None:
|
|
1345
|
+
minimum, maximum = torch.aminmax(tensor)
|
|
1346
|
+
if not bool(torch.eq(minimum, maximum).item()):
|
|
1347
|
+
return None
|
|
1348
|
+
return float(minimum.item())
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
def _store_coefficient_uniformity(solver) -> None:
|
|
1352
|
+
solver._coefficient_uniformity = {
|
|
1353
|
+
name: _uniform_coefficient_value(getattr(solver, name))
|
|
1354
|
+
for name in (
|
|
1355
|
+
"cex_decay",
|
|
1356
|
+
"cex_curl",
|
|
1357
|
+
"cey_decay",
|
|
1358
|
+
"cey_curl",
|
|
1359
|
+
"cez_decay",
|
|
1360
|
+
"cez_curl",
|
|
1361
|
+
"chx_decay",
|
|
1362
|
+
"chx_curl",
|
|
1363
|
+
"chy_decay",
|
|
1364
|
+
"chy_curl",
|
|
1365
|
+
"chz_decay",
|
|
1366
|
+
"chz_curl",
|
|
1367
|
+
)
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
|
|
1371
|
+
def build_update_coefficients(solver):
|
|
1372
|
+
eps_ex, eps_ey, eps_ez = _electric_epsilon_tensors(solver)
|
|
1373
|
+
# Under full anisotropy eps_e* is the conductively-shifted effective permittivity,
|
|
1374
|
+
# so the diagonal decay/curl must use the aniso_shifted semi-implicit identity
|
|
1375
|
+
# rather than re-applying the scalar (1 + half) denominator.
|
|
1376
|
+
aniso_shifted = getattr(solver, "full_aniso_enabled", False)
|
|
1377
|
+
if solver.active_absorber_type not in ("pml", "absorber"):
|
|
1378
|
+
solver.cex_decay, solver.cex_curl = _electric_update_coefficients(solver, eps_ex, solver.sigma_e_Ex, None, aniso_shifted=aniso_shifted)
|
|
1379
|
+
solver.cey_decay, solver.cey_curl = _electric_update_coefficients(solver, eps_ey, solver.sigma_e_Ey, None, aniso_shifted=aniso_shifted)
|
|
1380
|
+
solver.cez_decay, solver.cez_curl = _electric_update_coefficients(solver, eps_ez, solver.sigma_e_Ez, None, aniso_shifted=aniso_shifted)
|
|
1381
|
+
solver.chx_decay, solver.chx_curl = _magnetic_update_coefficients(solver, solver.mu_Hx, solver.sigma_m_Hx, None)
|
|
1382
|
+
solver.chy_decay, solver.chy_curl = _magnetic_update_coefficients(solver, solver.mu_Hy, solver.sigma_m_Hy, None)
|
|
1383
|
+
solver.chz_decay, solver.chz_curl = _magnetic_update_coefficients(solver, solver.mu_Hz, solver.sigma_m_Hz, None)
|
|
1384
|
+
_store_nonlinear_external_decay(solver, None, None, None)
|
|
1385
|
+
_build_full_aniso_curl_coefficients(solver)
|
|
1386
|
+
_apply_pec_edge_suppression(solver)
|
|
1387
|
+
_configure_sibc(solver)
|
|
1388
|
+
_store_coefficient_uniformity(solver)
|
|
1389
|
+
return
|
|
1390
|
+
|
|
1391
|
+
ex_sigma_y = 0.5 * (solver.sigma_y[:-1, :, :] + solver.sigma_y[1:, :, :])
|
|
1392
|
+
ex_sigma_z = 0.5 * (solver.sigma_z[:-1, :, :] + solver.sigma_z[1:, :, :])
|
|
1393
|
+
ex_decay = 1.0 / (1.0 + solver.dt * (ex_sigma_y + ex_sigma_z))
|
|
1394
|
+
solver.cex_decay, solver.cex_curl = _electric_update_coefficients(solver, eps_ex, solver.sigma_e_Ex, ex_decay, aniso_shifted=aniso_shifted)
|
|
1395
|
+
|
|
1396
|
+
ey_sigma_x = 0.5 * (solver.sigma_x[:, :-1, :] + solver.sigma_x[:, 1:, :])
|
|
1397
|
+
ey_sigma_z = 0.5 * (solver.sigma_z[:, :-1, :] + solver.sigma_z[:, 1:, :])
|
|
1398
|
+
ey_decay = 1.0 / (1.0 + solver.dt * (ey_sigma_x + ey_sigma_z))
|
|
1399
|
+
solver.cey_decay, solver.cey_curl = _electric_update_coefficients(solver, eps_ey, solver.sigma_e_Ey, ey_decay, aniso_shifted=aniso_shifted)
|
|
1400
|
+
|
|
1401
|
+
ez_sigma_x = 0.5 * (solver.sigma_x[:, :, :-1] + solver.sigma_x[:, :, 1:])
|
|
1402
|
+
ez_sigma_y = 0.5 * (solver.sigma_y[:, :, :-1] + solver.sigma_y[:, :, 1:])
|
|
1403
|
+
ez_decay = 1.0 / (1.0 + solver.dt * (ez_sigma_x + ez_sigma_y))
|
|
1404
|
+
solver.cez_decay, solver.cez_curl = _electric_update_coefficients(solver, eps_ez, solver.sigma_e_Ez, ez_decay, aniso_shifted=aniso_shifted)
|
|
1405
|
+
|
|
1406
|
+
hx_sigma_y = 0.25 * (
|
|
1407
|
+
solver.sigma_y[:, :-1, :-1]
|
|
1408
|
+
+ solver.sigma_y[:, 1:, :-1]
|
|
1409
|
+
+ solver.sigma_y[:, :-1, 1:]
|
|
1410
|
+
+ solver.sigma_y[:, 1:, 1:]
|
|
1411
|
+
)
|
|
1412
|
+
hx_sigma_z = 0.25 * (
|
|
1413
|
+
solver.sigma_z[:, :-1, :-1]
|
|
1414
|
+
+ solver.sigma_z[:, 1:, :-1]
|
|
1415
|
+
+ solver.sigma_z[:, :-1, 1:]
|
|
1416
|
+
+ solver.sigma_z[:, 1:, 1:]
|
|
1417
|
+
)
|
|
1418
|
+
hx_decay = 1.0 / (1.0 + solver.dt * (hx_sigma_y + hx_sigma_z))
|
|
1419
|
+
solver.chx_decay, solver.chx_curl = _magnetic_update_coefficients(solver, solver.mu_Hx, solver.sigma_m_Hx, hx_decay)
|
|
1420
|
+
|
|
1421
|
+
hy_sigma_x = 0.25 * (
|
|
1422
|
+
solver.sigma_x[:-1, :, :-1]
|
|
1423
|
+
+ solver.sigma_x[1:, :, :-1]
|
|
1424
|
+
+ solver.sigma_x[:-1, :, 1:]
|
|
1425
|
+
+ solver.sigma_x[1:, :, 1:]
|
|
1426
|
+
)
|
|
1427
|
+
hy_sigma_z = 0.25 * (
|
|
1428
|
+
solver.sigma_z[:-1, :, :-1]
|
|
1429
|
+
+ solver.sigma_z[1:, :, :-1]
|
|
1430
|
+
+ solver.sigma_z[:-1, :, 1:]
|
|
1431
|
+
+ solver.sigma_z[1:, :, 1:]
|
|
1432
|
+
)
|
|
1433
|
+
hy_decay = 1.0 / (1.0 + solver.dt * (hy_sigma_x + hy_sigma_z))
|
|
1434
|
+
solver.chy_decay, solver.chy_curl = _magnetic_update_coefficients(solver, solver.mu_Hy, solver.sigma_m_Hy, hy_decay)
|
|
1435
|
+
|
|
1436
|
+
hz_sigma_x = 0.25 * (
|
|
1437
|
+
solver.sigma_x[:-1, :-1, :]
|
|
1438
|
+
+ solver.sigma_x[1:, :-1, :]
|
|
1439
|
+
+ solver.sigma_x[:-1, 1:, :]
|
|
1440
|
+
+ solver.sigma_x[1:, 1:, :]
|
|
1441
|
+
)
|
|
1442
|
+
hz_sigma_y = 0.25 * (
|
|
1443
|
+
solver.sigma_y[:-1, :-1, :]
|
|
1444
|
+
+ solver.sigma_y[1:, :-1, :]
|
|
1445
|
+
+ solver.sigma_y[:-1, 1:, :]
|
|
1446
|
+
+ solver.sigma_y[1:, 1:, :]
|
|
1447
|
+
)
|
|
1448
|
+
hz_decay = 1.0 / (1.0 + solver.dt * (hz_sigma_x + hz_sigma_y))
|
|
1449
|
+
solver.chz_decay, solver.chz_curl = _magnetic_update_coefficients(solver, solver.mu_Hz, solver.sigma_m_Hz, hz_decay)
|
|
1450
|
+
|
|
1451
|
+
_store_nonlinear_external_decay(solver, ex_decay, ey_decay, ez_decay)
|
|
1452
|
+
_build_full_aniso_curl_coefficients(solver)
|
|
1453
|
+
_apply_pec_edge_suppression(solver)
|
|
1454
|
+
_configure_sibc(solver)
|
|
1455
|
+
_store_coefficient_uniformity(solver)
|
|
1456
|
+
|
|
1457
|
+
|
|
1458
|
+
def update_nonlinear_electric_coefficients(solver):
|
|
1459
|
+
"""Recompute the field-dependent electric update coefficients for this step.
|
|
1460
|
+
|
|
1461
|
+
Pure-chi3 (Kerr) scenes keep the existing curl-only kernel; scenes with a
|
|
1462
|
+
chi2 channel or field-dependent conductivity use the general nonlinear
|
|
1463
|
+
kernel that rewrites both the decay and curl coefficients.
|
|
1464
|
+
"""
|
|
1465
|
+
if not getattr(solver, "nonlinear_enabled", False):
|
|
1466
|
+
return
|
|
1467
|
+
if getattr(solver, "nonlinear_general_enabled", False):
|
|
1468
|
+
update_general_nonlinear_coefficients(solver)
|
|
1469
|
+
return
|
|
1470
|
+
update_kerr_electric_curls(solver)
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
def update_general_nonlinear_coefficients(solver):
|
|
1474
|
+
for component_name, decay_dynamic, curl_dynamic, eps, external, sigma_static, chi2, chi3, tpa in (
|
|
1475
|
+
(
|
|
1476
|
+
"Ex",
|
|
1477
|
+
solver.cex_decay_dynamic,
|
|
1478
|
+
solver.cex_curl_dynamic,
|
|
1479
|
+
solver.eps_Ex,
|
|
1480
|
+
solver.cex_decay_external,
|
|
1481
|
+
solver.sigma_e_Ex,
|
|
1482
|
+
solver.nonlinear_chi2_Ex,
|
|
1483
|
+
solver.kerr_chi3_Ex,
|
|
1484
|
+
solver.tpa_sigma_Ex,
|
|
1485
|
+
),
|
|
1486
|
+
(
|
|
1487
|
+
"Ey",
|
|
1488
|
+
solver.cey_decay_dynamic,
|
|
1489
|
+
solver.cey_curl_dynamic,
|
|
1490
|
+
solver.eps_Ey,
|
|
1491
|
+
solver.cey_decay_external,
|
|
1492
|
+
solver.sigma_e_Ey,
|
|
1493
|
+
solver.nonlinear_chi2_Ey,
|
|
1494
|
+
solver.kerr_chi3_Ey,
|
|
1495
|
+
solver.tpa_sigma_Ey,
|
|
1496
|
+
),
|
|
1497
|
+
(
|
|
1498
|
+
"Ez",
|
|
1499
|
+
solver.cez_decay_dynamic,
|
|
1500
|
+
solver.cez_curl_dynamic,
|
|
1501
|
+
solver.eps_Ez,
|
|
1502
|
+
solver.cez_decay_external,
|
|
1503
|
+
solver.sigma_e_Ez,
|
|
1504
|
+
solver.nonlinear_chi2_Ez,
|
|
1505
|
+
solver.kerr_chi3_Ez,
|
|
1506
|
+
solver.tpa_sigma_Ez,
|
|
1507
|
+
),
|
|
1508
|
+
):
|
|
1509
|
+
solver.fdtd_module.updateNonlinearElectricCoefficients3D(
|
|
1510
|
+
DynamicDecay=decay_dynamic,
|
|
1511
|
+
DynamicCurl=curl_dynamic,
|
|
1512
|
+
Ex=solver.Ex,
|
|
1513
|
+
Ey=solver.Ey,
|
|
1514
|
+
Ez=solver.Ez,
|
|
1515
|
+
LinearPermittivity=eps,
|
|
1516
|
+
ExternalDecay=external,
|
|
1517
|
+
SigmaStatic=sigma_static,
|
|
1518
|
+
Chi2=chi2,
|
|
1519
|
+
Chi3=chi3,
|
|
1520
|
+
TpaSigma=tpa,
|
|
1521
|
+
component=component_name,
|
|
1522
|
+
dt=solver.dt,
|
|
1523
|
+
eps0=solver.eps0,
|
|
1524
|
+
).launchRaw()
|
|
1525
|
+
|
|
1526
|
+
|
|
1527
|
+
def update_kerr_electric_curls(solver):
|
|
1528
|
+
if not solver.kerr_enabled:
|
|
1529
|
+
return
|
|
1530
|
+
|
|
1531
|
+
solver.fdtd_module.updateKerrElectricFieldExCurl3D(
|
|
1532
|
+
DynamicCurl=solver.cex_curl_dynamic,
|
|
1533
|
+
Ex=solver.Ex,
|
|
1534
|
+
Ey=solver.Ey,
|
|
1535
|
+
Ez=solver.Ez,
|
|
1536
|
+
LinearPermittivity=solver.eps_Ex,
|
|
1537
|
+
ExDecay=solver.cex_decay,
|
|
1538
|
+
KerrChi3=solver.kerr_chi3_Ex,
|
|
1539
|
+
dt=solver.dt,
|
|
1540
|
+
eps0=solver.eps0,
|
|
1541
|
+
).launchRaw()
|
|
1542
|
+
solver.fdtd_module.updateKerrElectricFieldEyCurl3D(
|
|
1543
|
+
DynamicCurl=solver.cey_curl_dynamic,
|
|
1544
|
+
Ex=solver.Ex,
|
|
1545
|
+
Ey=solver.Ey,
|
|
1546
|
+
Ez=solver.Ez,
|
|
1547
|
+
LinearPermittivity=solver.eps_Ey,
|
|
1548
|
+
EyDecay=solver.cey_decay,
|
|
1549
|
+
KerrChi3=solver.kerr_chi3_Ey,
|
|
1550
|
+
dt=solver.dt,
|
|
1551
|
+
eps0=solver.eps0,
|
|
1552
|
+
).launchRaw()
|
|
1553
|
+
solver.fdtd_module.updateKerrElectricFieldEzCurl3D(
|
|
1554
|
+
DynamicCurl=solver.cez_curl_dynamic,
|
|
1555
|
+
Ex=solver.Ex,
|
|
1556
|
+
Ey=solver.Ey,
|
|
1557
|
+
Ez=solver.Ez,
|
|
1558
|
+
LinearPermittivity=solver.eps_Ez,
|
|
1559
|
+
EzDecay=solver.cez_decay,
|
|
1560
|
+
KerrChi3=solver.kerr_chi3_Ez,
|
|
1561
|
+
dt=solver.dt,
|
|
1562
|
+
eps0=solver.eps0,
|
|
1563
|
+
).launchRaw()
|