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.
Files changed (107) hide show
  1. witwin/maxwell/__init__.py +164 -0
  2. witwin/maxwell/adapters/__init__.py +1 -0
  3. witwin/maxwell/adapters/gds.py +203 -0
  4. witwin/maxwell/adapters/tidy3d.py +1669 -0
  5. witwin/maxwell/adjoint_inputs.py +70 -0
  6. witwin/maxwell/compiler/__init__.py +11 -0
  7. witwin/maxwell/compiler/materials.py +1355 -0
  8. witwin/maxwell/compiler/monitors.py +164 -0
  9. witwin/maxwell/compiler/sources.py +222 -0
  10. witwin/maxwell/fdfd/__init__.py +3 -0
  11. witwin/maxwell/fdfd/adjoint/__init__.py +3 -0
  12. witwin/maxwell/fdfd/adjoint/bridge.py +180 -0
  13. witwin/maxwell/fdfd/ams.py +300 -0
  14. witwin/maxwell/fdfd/krylov.py +280 -0
  15. witwin/maxwell/fdfd/plotting.py +110 -0
  16. witwin/maxwell/fdfd/postprocess.py +33 -0
  17. witwin/maxwell/fdfd/solver.py +1057 -0
  18. witwin/maxwell/fdtd/__init__.py +4 -0
  19. witwin/maxwell/fdtd/adjoint/__init__.py +21 -0
  20. witwin/maxwell/fdtd/adjoint/bridge.py +746 -0
  21. witwin/maxwell/fdtd/adjoint/capabilities.py +29 -0
  22. witwin/maxwell/fdtd/adjoint/core.py +3614 -0
  23. witwin/maxwell/fdtd/adjoint/dispatch.py +684 -0
  24. witwin/maxwell/fdtd/adjoint/native.py +2561 -0
  25. witwin/maxwell/fdtd/adjoint/profiler.py +195 -0
  26. witwin/maxwell/fdtd/adjoint/reverse_common.py +207 -0
  27. witwin/maxwell/fdtd/adjoint/seeds.py +524 -0
  28. witwin/maxwell/fdtd/boundary/__init__.py +37 -0
  29. witwin/maxwell/fdtd/boundary/bloch.py +90 -0
  30. witwin/maxwell/fdtd/boundary/common.py +41 -0
  31. witwin/maxwell/fdtd/boundary/cpml.py +527 -0
  32. witwin/maxwell/fdtd/boundary/runtime.py +209 -0
  33. witwin/maxwell/fdtd/checkpoint.py +227 -0
  34. witwin/maxwell/fdtd/coords.py +32 -0
  35. witwin/maxwell/fdtd/cuda/__init__.py +11 -0
  36. witwin/maxwell/fdtd/cuda/backend.py +3532 -0
  37. witwin/maxwell/fdtd/cuda/build.py +292 -0
  38. witwin/maxwell/fdtd/cuda/extension.cpp +2053 -0
  39. witwin/maxwell/fdtd/cuda/kernels/adjoint.cu +5290 -0
  40. witwin/maxwell/fdtd/cuda/kernels/aniso.cu +1704 -0
  41. witwin/maxwell/fdtd/cuda/kernels/boundary.cu +424 -0
  42. witwin/maxwell/fdtd/cuda/kernels/common.cu +74 -0
  43. witwin/maxwell/fdtd/cuda/kernels/common.cuh +82 -0
  44. witwin/maxwell/fdtd/cuda/kernels/dispersive.cu +782 -0
  45. witwin/maxwell/fdtd/cuda/kernels/electric.cu +3613 -0
  46. witwin/maxwell/fdtd/cuda/kernels/magnetic.cu +781 -0
  47. witwin/maxwell/fdtd/cuda/kernels/observers.cu +347 -0
  48. witwin/maxwell/fdtd/cuda/kernels/projection.cu +211 -0
  49. witwin/maxwell/fdtd/cuda/kernels/sources.cu +1482 -0
  50. witwin/maxwell/fdtd/cuda/kernels/spectral.cu +297 -0
  51. witwin/maxwell/fdtd/cuda/launch.h +23 -0
  52. witwin/maxwell/fdtd/cuda/prebuilt/witwin_maxwell_fdtd_cuda.pyd +0 -0
  53. witwin/maxwell/fdtd/cuda/runtime/__init__.py +6 -0
  54. witwin/maxwell/fdtd/cuda/runtime/dispatcher.py +6 -0
  55. witwin/maxwell/fdtd/cuda/runtime/graph.py +33 -0
  56. witwin/maxwell/fdtd/cuda/tensors.h +35 -0
  57. witwin/maxwell/fdtd/dispersion.py +48 -0
  58. witwin/maxwell/fdtd/evaluation.py +290 -0
  59. witwin/maxwell/fdtd/excitation/__init__.py +27 -0
  60. witwin/maxwell/fdtd/excitation/injection.py +1102 -0
  61. witwin/maxwell/fdtd/excitation/modes.py +1675 -0
  62. witwin/maxwell/fdtd/excitation/spatial.py +469 -0
  63. witwin/maxwell/fdtd/excitation/temporal.py +510 -0
  64. witwin/maxwell/fdtd/excitation/tfsf_apply.py +264 -0
  65. witwin/maxwell/fdtd/excitation/tfsf_common.py +421 -0
  66. witwin/maxwell/fdtd/excitation/tfsf_specs.py +490 -0
  67. witwin/maxwell/fdtd/excitation/tfsf_state.py +778 -0
  68. witwin/maxwell/fdtd/io.py +31 -0
  69. witwin/maxwell/fdtd/material_pullback.py +371 -0
  70. witwin/maxwell/fdtd/meshing/__init__.py +3 -0
  71. witwin/maxwell/fdtd/meshing/autogrid.py +335 -0
  72. witwin/maxwell/fdtd/observers.py +1453 -0
  73. witwin/maxwell/fdtd/plotting.py +121 -0
  74. witwin/maxwell/fdtd/postprocess.py +205 -0
  75. witwin/maxwell/fdtd/runtime/__init__.py +119 -0
  76. witwin/maxwell/fdtd/runtime/initialization.py +236 -0
  77. witwin/maxwell/fdtd/runtime/materials.py +1563 -0
  78. witwin/maxwell/fdtd/runtime/module_cache.py +31 -0
  79. witwin/maxwell/fdtd/runtime/spectral.py +581 -0
  80. witwin/maxwell/fdtd/runtime/stepping.py +1987 -0
  81. witwin/maxwell/fdtd/solver.py +502 -0
  82. witwin/maxwell/geometry/__init__.py +5 -0
  83. witwin/maxwell/geometry/polyslab.py +299 -0
  84. witwin/maxwell/materials.py +3 -0
  85. witwin/maxwell/media.py +1467 -0
  86. witwin/maxwell/monitors.py +743 -0
  87. witwin/maxwell/ports.py +113 -0
  88. witwin/maxwell/postprocess/RCS.py +139 -0
  89. witwin/maxwell/postprocess/__init__.py +42 -0
  90. witwin/maxwell/postprocess/diffraction.py +219 -0
  91. witwin/maxwell/postprocess/directivity.py +152 -0
  92. witwin/maxwell/postprocess/emission.py +67 -0
  93. witwin/maxwell/postprocess/modal.py +324 -0
  94. witwin/maxwell/postprocess/nfft.py +266 -0
  95. witwin/maxwell/postprocess/scattering_parameters.py +358 -0
  96. witwin/maxwell/postprocess/stratton_chu.py +1193 -0
  97. witwin/maxwell/result.py +967 -0
  98. witwin/maxwell/scene.py +1338 -0
  99. witwin/maxwell/simulation.py +659 -0
  100. witwin/maxwell/sources.py +917 -0
  101. witwin/maxwell/visualization/__init__.py +28 -0
  102. witwin/maxwell/visualization/interactive.py +484 -0
  103. witwin/maxwell/visualization/plots.py +568 -0
  104. witwin_maxwell-0.3.0.dist-info/METADATA +184 -0
  105. witwin_maxwell-0.3.0.dist-info/RECORD +107 -0
  106. witwin_maxwell-0.3.0.dist-info/WHEEL +4 -0
  107. witwin_maxwell-0.3.0.dist-info/licenses/LICENSE +13 -0
@@ -0,0 +1,527 @@
1
+ import math
2
+
3
+ import numpy as np
4
+ import torch
5
+
6
+ from .common import BOUNDARY_PML
7
+
8
+
9
+ DEFAULT_CPML_CONFIG = {
10
+ # A cubic, impedance-matched conductivity ramp is the robust default for
11
+ # the relatively thin (8-12 cell) layers used by Scene boundaries. Large
12
+ # kappa stretching at this thickness creates a resolvable entrance mismatch
13
+ # for propagating waves; complex-frequency shifting remains available through
14
+ # cpml_config and StablePML for low-frequency/late-time workloads.
15
+ "grading_order": 3.0,
16
+ "kappa_max": 1.0,
17
+ "alpha_max": 0.0,
18
+ "reflection": 1e-6,
19
+ "memory_mode": "auto",
20
+ "dense_memory_limit_mib": 64.0,
21
+ }
22
+
23
+ # StablePML overrides: a higher grading order and a larger complex-frequency
24
+ # shift (alpha_max) trade a little absorption for markedly improved late-time
25
+ # stability, which suppresses the low-frequency growth that stresses a plain CPML.
26
+ STABLE_PML_CONFIG_OVERRIDES = {
27
+ "grading_order": 5.0,
28
+ "kappa_max": 5.0,
29
+ "alpha_max": 0.24,
30
+ "reflection": 1e-9,
31
+ }
32
+
33
+ # Adiabatic absorber grading: gentle cubic conductivity ramp, no kappa/alpha
34
+ # stretching and no auxiliary psi memory.
35
+ ABSORBER_GRADING_ORDER = 3.0
36
+ ABSORBER_REFLECTION = 1e-6
37
+
38
+ _AUTO_CPML_DENSE_FREE_FRACTION = 0.125
39
+
40
+ _CPML_VECTOR_ATTRS = (
41
+ "cpml_kappa_e_x",
42
+ "cpml_kappa_e_y",
43
+ "cpml_kappa_e_z",
44
+ "cpml_inv_kappa_e_x",
45
+ "cpml_inv_kappa_e_y",
46
+ "cpml_inv_kappa_e_z",
47
+ "cpml_b_e_x",
48
+ "cpml_b_e_y",
49
+ "cpml_b_e_z",
50
+ "cpml_c_e_x",
51
+ "cpml_c_e_y",
52
+ "cpml_c_e_z",
53
+ "cpml_kappa_h_x",
54
+ "cpml_kappa_h_y",
55
+ "cpml_kappa_h_z",
56
+ "cpml_inv_kappa_h_x",
57
+ "cpml_inv_kappa_h_y",
58
+ "cpml_inv_kappa_h_z",
59
+ "cpml_b_h_x",
60
+ "cpml_b_h_y",
61
+ "cpml_b_h_z",
62
+ "cpml_c_h_x",
63
+ "cpml_c_h_y",
64
+ "cpml_c_h_z",
65
+ )
66
+
67
+ _CPML_MEMORY_ATTRS = (
68
+ "psi_ex_y",
69
+ "psi_ex_z",
70
+ "psi_ey_x",
71
+ "psi_ey_z",
72
+ "psi_ez_x",
73
+ "psi_ez_y",
74
+ "psi_hx_y",
75
+ "psi_hx_z",
76
+ "psi_hy_x",
77
+ "psi_hy_z",
78
+ "psi_hz_x",
79
+ "psi_hz_y",
80
+ )
81
+ _CPML_IMAG_MEMORY_ATTRS = tuple(f"{attr_name}_imag" for attr_name in _CPML_MEMORY_ATTRS)
82
+
83
+ _CPML_MEMORY_SPECS = {
84
+ "psi_ex_y": ("Ex", 1),
85
+ "psi_ex_z": ("Ex", 2),
86
+ "psi_ey_x": ("Ey", 0),
87
+ "psi_ey_z": ("Ey", 2),
88
+ "psi_ez_x": ("Ez", 0),
89
+ "psi_ez_y": ("Ez", 1),
90
+ "psi_hx_y": ("Hx", 1),
91
+ "psi_hx_z": ("Hx", 2),
92
+ "psi_hy_x": ("Hy", 0),
93
+ "psi_hy_z": ("Hy", 2),
94
+ "psi_hz_x": ("Hz", 0),
95
+ "psi_hz_y": ("Hz", 1),
96
+ }
97
+
98
+ _SIGMA_ATTRS = (
99
+ "sigma_x",
100
+ "sigma_y",
101
+ "sigma_z",
102
+ )
103
+
104
+
105
+ def _legacy_strength_scale(strength):
106
+ value = float(strength)
107
+ if value <= 0:
108
+ return 1.0
109
+ if value > 100.0:
110
+ return 1.0
111
+ return value
112
+
113
+
114
+ def _cpml_profile_1d(
115
+ positions64,
116
+ nodes64,
117
+ thickness,
118
+ dt,
119
+ vacuum_constant,
120
+ sigma_scale,
121
+ impedance,
122
+ config,
123
+ *,
124
+ high_positions64=None,
125
+ apply_low=True,
126
+ apply_high=True,
127
+ ):
128
+ """Build (kappa, b, c) CPML profile vectors graded by physical depth.
129
+
130
+ ``positions64`` are the float64 sample positions of the profile (node
131
+ masters for E profiles, half-point masters for H profiles) and ``nodes64``
132
+ the axis node masters; ``thickness`` is the PML thickness in cells. The
133
+ per-side layer depth and peak conductivity derive from the physical layer
134
+ extents, so nonuniform grids grade correctly and uniform grids reproduce
135
+ the established index-based grading. ``high_positions64`` overrides the
136
+ sample positions used for the high-side depth (H profiles pass their
137
+ cell's outer node so the outermost H sample reaches full depth, matching
138
+ the calibrated legacy grading). Grading runs in float64; the returned
139
+ vectors are float32 on the device of ``sigma_scale``.
140
+ """
141
+ length = int(len(positions64))
142
+ device = sigma_scale.device
143
+
144
+ def _to_device(array):
145
+ return torch.tensor(array, dtype=torch.float32, device=device).contiguous()
146
+
147
+ thickness = int(thickness)
148
+ if thickness <= 0:
149
+ return (
150
+ _to_device(np.ones(length)),
151
+ _to_device(np.zeros(length)),
152
+ _to_device(np.zeros(length)),
153
+ )
154
+
155
+ def _interface(*, low):
156
+ # Layer interface measured `thickness` cells inward from the boundary.
157
+ # When the layer is thicker than the axis, extrapolate past the far
158
+ # end with the far-edge cell spacing so degenerate all-PML domains
159
+ # keep the established partial-depth grading.
160
+ cell_count = len(nodes64) - 1
161
+ if thickness <= cell_count:
162
+ return float(nodes64[thickness]) if low else float(nodes64[-1 - thickness])
163
+ extra = thickness - cell_count
164
+ if low:
165
+ return float(nodes64[-1]) + extra * float(nodes64[-1] - nodes64[-2])
166
+ return float(nodes64[0]) - extra * float(nodes64[1] - nodes64[0])
167
+
168
+ grading_order = float(config["grading_order"])
169
+ kappa_max = float(config["kappa_max"])
170
+ alpha_max = float(config["alpha_max"])
171
+ reflection = float(config["reflection"])
172
+ strength = float(sigma_scale.item())
173
+ sigma_numerator = -(grading_order + 1.0) * math.log(reflection) / (2.0 * float(impedance))
174
+
175
+ positions = np.asarray(positions64, dtype=np.float64)
176
+ high_positions = positions if high_positions64 is None else np.asarray(high_positions64, dtype=np.float64)
177
+ depth_low = np.zeros(length, dtype=np.float64)
178
+ depth_high = np.zeros(length, dtype=np.float64)
179
+ sigma_max_low = 0.0
180
+ sigma_max_high = 0.0
181
+ if apply_low:
182
+ low_interface = _interface(low=True)
183
+ layer_low = low_interface - float(nodes64[0])
184
+ depth_low = np.clip((low_interface - positions) / layer_low, 0.0, 1.0)
185
+ sigma_max_low = sigma_numerator / layer_low * strength
186
+ if apply_high:
187
+ high_interface = _interface(low=False)
188
+ layer_high = float(nodes64[-1]) - high_interface
189
+ depth_high = np.clip((high_positions - high_interface) / layer_high, 0.0, 1.0)
190
+ sigma_max_high = sigma_numerator / layer_high * strength
191
+
192
+ use_high = depth_high > depth_low
193
+ depth = np.where(use_high, depth_high, depth_low)
194
+ sigma_peak = np.where(use_high, sigma_max_high, sigma_max_low)
195
+
196
+ active = depth > 0.0
197
+ graded = np.zeros(length, dtype=np.float64)
198
+ graded[active] = depth[active] ** grading_order
199
+ sigma = sigma_peak * graded
200
+ kappa = np.ones(length, dtype=np.float64)
201
+ kappa[active] = 1.0 + (kappa_max - 1.0) * graded[active]
202
+ alpha = np.zeros(length, dtype=np.float64)
203
+ alpha[active] = alpha_max * (1.0 - depth[active])
204
+
205
+ decay = np.exp(-(sigma / kappa + alpha) * float(dt) / float(vacuum_constant))
206
+ denom = sigma + alpha * kappa
207
+ c = np.zeros(length, dtype=np.float64)
208
+ mask = denom > 1e-12
209
+ c[mask] = sigma[mask] * (decay[mask] - 1.0) / (denom[mask] * kappa[mask])
210
+ return _to_device(kappa), _to_device(decay), _to_device(c)
211
+
212
+
213
+ def _axis_face_uses_pml(solver, axis: str, *, low: bool) -> bool:
214
+ attr_name = f"boundary_{axis}_{'low' if low else 'high'}_code"
215
+ return int(getattr(solver, attr_name, solver.boundary_code)) == BOUNDARY_PML
216
+
217
+
218
+ def _build_cpml_memory_layout(solver, attr_name):
219
+ field_name, axis = _CPML_MEMORY_SPECS[attr_name]
220
+ field = getattr(solver, field_name)
221
+ axis_name = "xyz"[axis]
222
+ field_shape = tuple(int(size) for size in field.shape)
223
+ axis_size = int(field_shape[axis])
224
+ thickness = min(int(solver.scene.pml_thickness), axis_size)
225
+ low_length = thickness if _axis_face_uses_pml(solver, axis_name, low=True) else 0
226
+ high_capacity = max(axis_size - low_length, 0)
227
+ high_length = min(thickness if _axis_face_uses_pml(solver, axis_name, low=False) else 0, high_capacity)
228
+ compressed_shape = list(field_shape)
229
+ compressed_shape[axis] = low_length + high_length
230
+ layout = {
231
+ "field_name": field_name,
232
+ "field_shape": field_shape,
233
+ "compressed_shape": tuple(compressed_shape),
234
+ "axis": axis,
235
+ "regions": [],
236
+ }
237
+ local_start = 0
238
+ if low_length > 0:
239
+ layout["regions"].append(
240
+ {
241
+ "side": "low",
242
+ "global_start": 0,
243
+ "length": low_length,
244
+ "local_start": local_start,
245
+ }
246
+ )
247
+ local_start += low_length
248
+ if high_length > 0:
249
+ layout["regions"].append(
250
+ {
251
+ "side": "high",
252
+ "global_start": axis_size - high_length,
253
+ "length": high_length,
254
+ "local_start": local_start,
255
+ }
256
+ )
257
+ return layout
258
+
259
+
260
+ def _estimate_cpml_memory_bytes(layouts, solver):
261
+ dense_bytes = 0
262
+ slab_bytes = 0
263
+ for layout in layouts.values():
264
+ field = getattr(solver, layout["field_name"])
265
+ element_bytes = int(field.element_size())
266
+ dense_bytes += math.prod(layout["field_shape"]) * element_bytes
267
+ slab_bytes += math.prod(layout["compressed_shape"]) * element_bytes
268
+ return dense_bytes, slab_bytes
269
+
270
+
271
+ def _requested_cpml_memory_mode(solver):
272
+ requested = str(solver.cpml_config.get("memory_mode", "auto")).strip().lower()
273
+ if requested not in {"auto", "dense", "slab"}:
274
+ raise ValueError(
275
+ "cpml_config['memory_mode'] must be one of 'auto', 'dense', or 'slab'."
276
+ )
277
+ return requested
278
+
279
+
280
+ def _resolve_cpml_memory_mode(solver, dense_bytes):
281
+ requested = _requested_cpml_memory_mode(solver)
282
+ solver._cpml_memory_mode_requested = requested
283
+ if requested != "auto":
284
+ solver._cpml_dense_memory_limit_bytes = None
285
+ solver._cpml_auto_free_bytes = None
286
+ return requested
287
+
288
+ limit_mib = float(
289
+ solver.cpml_config.get("dense_memory_limit_mib", DEFAULT_CPML_CONFIG["dense_memory_limit_mib"])
290
+ )
291
+ limit_bytes = max(int(limit_mib * 1024 * 1024), 0)
292
+ free_bytes = None
293
+ try:
294
+ free_bytes, _ = torch.cuda.mem_get_info(solver.device)
295
+ except (RuntimeError, ValueError):
296
+ free_bytes = None
297
+ if free_bytes is not None:
298
+ limit_bytes = min(limit_bytes, int(float(free_bytes) * _AUTO_CPML_DENSE_FREE_FRACTION))
299
+ solver._cpml_dense_memory_limit_bytes = limit_bytes
300
+ solver._cpml_auto_free_bytes = None if free_bytes is None else int(free_bytes)
301
+ return "dense" if dense_bytes <= limit_bytes else "slab"
302
+
303
+
304
+ def _allocate_cpml_memory_variables(solver):
305
+ layouts = {attr_name: _build_cpml_memory_layout(solver, attr_name) for attr_name in _CPML_MEMORY_ATTRS}
306
+ dense_bytes, slab_bytes = _estimate_cpml_memory_bytes(layouts, solver)
307
+ memory_multiplier = 2 if bool(getattr(solver, "complex_fields_enabled", False)) else 1
308
+ dense_bytes *= memory_multiplier
309
+ slab_bytes *= memory_multiplier
310
+ memory_mode = _resolve_cpml_memory_mode(solver, dense_bytes)
311
+
312
+ solver._cpml_memory_layouts = {}
313
+ solver._cpml_memory_mode = memory_mode
314
+ solver._cpml_dense_memory_bytes = dense_bytes
315
+ solver._cpml_slab_memory_bytes = slab_bytes
316
+ solver._cpml_allocated_memory_bytes = 0
317
+
318
+ for attr_name, layout in layouts.items():
319
+ field = getattr(solver, layout["field_name"])
320
+ target_shape = layout["field_shape"] if memory_mode == "dense" else layout["compressed_shape"]
321
+ tensor = torch.zeros(target_shape, device=solver.device, dtype=field.dtype)
322
+ if memory_mode == "slab":
323
+ solver._cpml_memory_layouts[attr_name] = layout
324
+ solver._cpml_allocated_memory_bytes += tensor.numel() * tensor.element_size()
325
+ setattr(solver, attr_name, tensor)
326
+ if bool(getattr(solver, "complex_fields_enabled", False)):
327
+ imag_attr_name = f"{attr_name}_imag"
328
+ imag_tensor = torch.zeros(target_shape, device=solver.device, dtype=field.dtype)
329
+ if memory_mode == "slab":
330
+ solver._cpml_memory_layouts[imag_attr_name] = layout
331
+ solver._cpml_allocated_memory_bytes += imag_tensor.numel() * imag_tensor.element_size()
332
+ setattr(solver, imag_attr_name, imag_tensor)
333
+
334
+
335
+ def expand_cpml_memory_tensor(solver, attr_name):
336
+ tensor = getattr(solver, attr_name)
337
+ layout = getattr(solver, "_cpml_memory_layouts", {}).get(attr_name)
338
+ if layout is None:
339
+ return tensor
340
+
341
+ dense = torch.zeros(layout["field_shape"], device=tensor.device, dtype=tensor.dtype)
342
+ axis = int(layout["axis"])
343
+ for region in layout["regions"]:
344
+ length = int(region["length"])
345
+ if length <= 0:
346
+ continue
347
+ dense.narrow(axis, int(region["global_start"]), length).copy_(
348
+ tensor.narrow(axis, int(region["local_start"]), length)
349
+ )
350
+ return dense
351
+
352
+
353
+ def _initialize_neutral_cpml_buffers(solver):
354
+ solver.cpml_kappa_e_x = torch.ones(solver.Nx, device=solver.device, dtype=torch.float32)
355
+ solver.cpml_kappa_e_y = torch.ones(solver.Ny, device=solver.device, dtype=torch.float32)
356
+ solver.cpml_kappa_e_z = torch.ones(solver.Nz, device=solver.device, dtype=torch.float32)
357
+ solver.cpml_b_e_x = torch.zeros(solver.Nx, device=solver.device, dtype=torch.float32)
358
+ solver.cpml_b_e_y = torch.zeros(solver.Ny, device=solver.device, dtype=torch.float32)
359
+ solver.cpml_b_e_z = torch.zeros(solver.Nz, device=solver.device, dtype=torch.float32)
360
+ solver.cpml_c_e_x = torch.zeros(solver.Nx, device=solver.device, dtype=torch.float32)
361
+ solver.cpml_c_e_y = torch.zeros(solver.Ny, device=solver.device, dtype=torch.float32)
362
+ solver.cpml_c_e_z = torch.zeros(solver.Nz, device=solver.device, dtype=torch.float32)
363
+
364
+ solver.cpml_kappa_h_x = torch.ones(solver.Nx - 1, device=solver.device, dtype=torch.float32)
365
+ solver.cpml_kappa_h_y = torch.ones(solver.Ny - 1, device=solver.device, dtype=torch.float32)
366
+ solver.cpml_kappa_h_z = torch.ones(solver.Nz - 1, device=solver.device, dtype=torch.float32)
367
+ solver.cpml_b_h_x = torch.zeros(solver.Nx - 1, device=solver.device, dtype=torch.float32)
368
+ solver.cpml_b_h_y = torch.zeros(solver.Ny - 1, device=solver.device, dtype=torch.float32)
369
+ solver.cpml_b_h_z = torch.zeros(solver.Nz - 1, device=solver.device, dtype=torch.float32)
370
+ solver.cpml_c_h_x = torch.zeros(solver.Nx - 1, device=solver.device, dtype=torch.float32)
371
+ solver.cpml_c_h_y = torch.zeros(solver.Ny - 1, device=solver.device, dtype=torch.float32)
372
+ solver.cpml_c_h_z = torch.zeros(solver.Nz - 1, device=solver.device, dtype=torch.float32)
373
+
374
+
375
+ def _allocate_sigma_tensors(solver):
376
+ solver.sigma_x = torch.zeros((solver.Nx, solver.Ny, solver.Nz), device=solver.device, dtype=torch.float32)
377
+ solver.sigma_y = torch.zeros((solver.Nx, solver.Ny, solver.Nz), device=solver.device, dtype=torch.float32)
378
+ solver.sigma_z = torch.zeros((solver.Nx, solver.Ny, solver.Nz), device=solver.device, dtype=torch.float32)
379
+
380
+
381
+ def _set_inv_kappa_buffers(solver):
382
+ solver.cpml_inv_kappa_e_x = torch.reciprocal(solver.cpml_kappa_e_x).contiguous()
383
+ solver.cpml_inv_kappa_e_y = torch.reciprocal(solver.cpml_kappa_e_y).contiguous()
384
+ solver.cpml_inv_kappa_e_z = torch.reciprocal(solver.cpml_kappa_e_z).contiguous()
385
+ solver.cpml_inv_kappa_h_x = torch.reciprocal(solver.cpml_kappa_h_x).contiguous()
386
+ solver.cpml_inv_kappa_h_y = torch.reciprocal(solver.cpml_kappa_h_y).contiguous()
387
+ solver.cpml_inv_kappa_h_z = torch.reciprocal(solver.cpml_kappa_h_z).contiguous()
388
+
389
+
390
+ def _clear_auxiliary_state(solver):
391
+ for attr in _CPML_VECTOR_ATTRS + _CPML_MEMORY_ATTRS + _CPML_IMAG_MEMORY_ATTRS + _SIGMA_ATTRS:
392
+ setattr(solver, attr, None)
393
+ solver._cpml_memory_layouts = {}
394
+ solver._cpml_memory_mode = "none"
395
+ solver._cpml_dense_memory_bytes = 0
396
+ solver._cpml_slab_memory_bytes = 0
397
+ solver._cpml_allocated_memory_bytes = 0
398
+ solver._cpml_dense_memory_limit_bytes = None
399
+ solver._cpml_auto_free_bytes = None
400
+
401
+
402
+ def build_cpml_profiles(solver):
403
+ strength_scale = _legacy_strength_scale(solver.scene.pml_strength)
404
+ scale_tensor = torch.tensor(strength_scale, device=solver.device, dtype=torch.float32)
405
+ magnetic_scale = scale_tensor * (solver.mu0 / solver.eps0)
406
+ thickness = solver.scene.pml_thickness
407
+ eta0 = math.sqrt(solver.mu0 / solver.eps0)
408
+ scene = solver.scene
409
+
410
+ axis_masters = {
411
+ "x": (scene.x_nodes64, scene.x_half64),
412
+ "y": (scene.y_nodes64, scene.y_half64),
413
+ "z": (scene.z_nodes64, scene.z_half64),
414
+ }
415
+ for axis, (nodes64, half64) in axis_masters.items():
416
+ apply_low = _axis_face_uses_pml(solver, axis, low=True)
417
+ apply_high = _axis_face_uses_pml(solver, axis, low=False)
418
+ kappa_e, b_e, c_e = _cpml_profile_1d(
419
+ nodes64,
420
+ nodes64,
421
+ thickness,
422
+ solver.dt,
423
+ solver.eps0,
424
+ scale_tensor,
425
+ eta0,
426
+ solver.cpml_config,
427
+ apply_low=apply_low,
428
+ apply_high=apply_high,
429
+ )
430
+ kappa_h, b_h, c_h = _cpml_profile_1d(
431
+ half64,
432
+ nodes64,
433
+ thickness,
434
+ solver.dt,
435
+ solver.mu0,
436
+ magnetic_scale,
437
+ eta0,
438
+ solver.cpml_config,
439
+ high_positions64=nodes64[1:],
440
+ apply_low=apply_low,
441
+ apply_high=apply_high,
442
+ )
443
+ setattr(solver, f"cpml_kappa_e_{axis}", kappa_e)
444
+ setattr(solver, f"cpml_b_e_{axis}", b_e)
445
+ setattr(solver, f"cpml_c_e_{axis}", c_e)
446
+ setattr(solver, f"cpml_kappa_h_{axis}", kappa_h)
447
+ setattr(solver, f"cpml_b_h_{axis}", b_h)
448
+ setattr(solver, f"cpml_c_h_{axis}", c_h)
449
+
450
+
451
+ def initialize_cpml_state(solver):
452
+ _clear_auxiliary_state(solver)
453
+ build_cpml_profiles(solver)
454
+ _set_inv_kappa_buffers(solver)
455
+ _allocate_cpml_memory_variables(solver)
456
+
457
+
458
+ def initialize_stable_pml_state(solver):
459
+ # StablePML reuses the full CPML machinery with a stability-oriented profile.
460
+ config = dict(solver.cpml_config)
461
+ config.update(STABLE_PML_CONFIG_OVERRIDES)
462
+ solver.cpml_config = config
463
+ initialize_cpml_state(solver)
464
+
465
+
466
+ def initialize_neutral_boundary_state(solver):
467
+ _clear_auxiliary_state(solver)
468
+
469
+
470
+ def initialize_simple_pml_state(solver):
471
+ _clear_auxiliary_state(solver)
472
+
473
+ thickness = solver.scene.pml_thickness
474
+ _allocate_sigma_tensors(solver)
475
+ sigma_max = solver.scene.pml_strength * 2.0 / (solver.dt * min(solver.min_dx, solver.min_dy, solver.min_dz))
476
+ for i in range(thickness):
477
+ x = (thickness - i) / thickness
478
+ sigma = sigma_max * x**4
479
+ if _axis_face_uses_pml(solver, "x", low=True):
480
+ solver.sigma_x[i, :, :] = sigma
481
+ if _axis_face_uses_pml(solver, "x", low=False):
482
+ solver.sigma_x[-(i + 1), :, :] = sigma
483
+ if _axis_face_uses_pml(solver, "y", low=True):
484
+ solver.sigma_y[:, i, :] = sigma
485
+ if _axis_face_uses_pml(solver, "y", low=False):
486
+ solver.sigma_y[:, -(i + 1), :] = sigma
487
+ if _axis_face_uses_pml(solver, "z", low=True):
488
+ solver.sigma_z[:, :, i] = sigma
489
+ if _axis_face_uses_pml(solver, "z", low=False):
490
+ solver.sigma_z[:, :, -(i + 1)] = sigma
491
+
492
+
493
+ def initialize_absorber_state(solver):
494
+ # Adiabatic graded-conductivity absorber. Shares the semi-implicit sigma
495
+ # branch used by the simple PML but with a gentler cubic ramp and a
496
+ # reflection-calibrated peak conductivity, and no kappa/alpha stretching or
497
+ # auxiliary psi memory.
498
+ _clear_auxiliary_state(solver)
499
+
500
+ thickness = solver.scene.pml_thickness
501
+ _allocate_sigma_tensors(solver)
502
+ eta0 = math.sqrt(solver.mu0 / solver.eps0)
503
+ grading_order = ABSORBER_GRADING_ORDER
504
+ min_delta = min(solver.min_dx, solver.min_dy, solver.min_dz)
505
+ # Physical peak conductivity (S/m) calibrated to the target round-trip
506
+ # reflection, then converted to the 1/s scale that the semi-implicit sigma
507
+ # update expects (the stored sigma multiplies dt directly, i.e. sigma/eps0).
508
+ sigma_phys_max = -(grading_order + 1.0) * math.log(ABSORBER_REFLECTION) / (
509
+ 2.0 * eta0 * thickness * min_delta
510
+ )
511
+ sigma_max = sigma_phys_max / solver.eps0
512
+ sigma_max *= _legacy_strength_scale(solver.scene.pml_strength)
513
+ for i in range(thickness):
514
+ x = (thickness - i) / thickness
515
+ sigma = sigma_max * x**grading_order
516
+ if _axis_face_uses_pml(solver, "x", low=True):
517
+ solver.sigma_x[i, :, :] = sigma
518
+ if _axis_face_uses_pml(solver, "x", low=False):
519
+ solver.sigma_x[-(i + 1), :, :] = sigma
520
+ if _axis_face_uses_pml(solver, "y", low=True):
521
+ solver.sigma_y[:, i, :] = sigma
522
+ if _axis_face_uses_pml(solver, "y", low=False):
523
+ solver.sigma_y[:, -(i + 1), :] = sigma
524
+ if _axis_face_uses_pml(solver, "z", low=True):
525
+ solver.sigma_z[:, :, i] = sigma
526
+ if _axis_face_uses_pml(solver, "z", low=False):
527
+ solver.sigma_z[:, :, -(i + 1)] = sigma