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,469 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import os
5
+
6
+ import numpy as np
7
+ import torch
8
+
9
+ from ...scene import prepare_scene
10
+ from ...sources import evaluate_source_time
11
+
12
+
13
+ _AXIS_TO_INDEX = {"x": 0, "y": 1, "z": 2}
14
+
15
+ # Relative spacing spread at or below which a tangential aperture window is
16
+ # treated as exactly uniform: the exact minimum cell spacing is returned
17
+ # bit-for-bit (equal to the global minimum used before), so uniform grids keep
18
+ # their numerical-dispersion phase correction unchanged.
19
+ _SOFT_PLANE_UNIFORM_RTOL = 1e-6
20
+
21
+
22
+ def _pml_margin(scene, axis: str, side: str, *, extra: int = 0) -> int:
23
+ base = scene.pml_thickness_for_face(axis, side)
24
+ return int(base) + int(extra)
25
+
26
+
27
+ def resolve_injection_axis(direction, injection_axis=None) -> str:
28
+ if injection_axis is not None:
29
+ axis = str(injection_axis).lower()
30
+ if axis not in _AXIS_TO_INDEX:
31
+ raise ValueError("injection_axis must be 'x', 'y', 'z', or None.")
32
+ axis_index = _AXIS_TO_INDEX[axis]
33
+ if abs(float(direction[axis_index])) <= 1e-6:
34
+ raise ValueError("direction must have a non-zero component along injection_axis.")
35
+ return axis
36
+
37
+ dominant_index = max(range(3), key=lambda index: abs(float(direction[index])))
38
+ if abs(float(direction[dominant_index])) <= 1e-6:
39
+ raise ValueError("direction must be non-zero.")
40
+ return "xyz"[dominant_index]
41
+
42
+
43
+ def source_plane_index(scene, axis: str, direction_component: float) -> int:
44
+ scene = prepare_scene(scene)
45
+ axis_index = _AXIS_TO_INDEX[axis]
46
+ domain_counts = (scene.Nx, scene.Ny, scene.Nz)
47
+ low_margin = min(_pml_margin(scene, axis, "low", extra=1), max(domain_counts[axis_index] - 1, 0))
48
+ high_margin = min(_pml_margin(scene, axis, "high", extra=1), max(domain_counts[axis_index] - 1, 0))
49
+ if direction_component >= 0.0:
50
+ return low_margin
51
+ return max(domain_counts[axis_index] - high_margin - 1, 0)
52
+
53
+
54
+ def physical_interior_indices(scene, axis: str) -> tuple[int, int]:
55
+ scene = prepare_scene(scene)
56
+ size = {"x": scene.Nx, "y": scene.Ny, "z": scene.Nz}[axis]
57
+ lo = _pml_margin(scene, axis, "low")
58
+ hi = size - _pml_margin(scene, axis, "high") - 1
59
+ if hi <= lo:
60
+ raise ValueError(f"Scene has no physical interior along {axis}.")
61
+ return lo, hi
62
+
63
+
64
+ def soft_plane_wave_index(scene, axis: str, direction_component: float, *, fraction: float = 0.05) -> int:
65
+ scene = prepare_scene(scene)
66
+ axis_coords = {"x": scene.x, "y": scene.y, "z": scene.z}[axis]
67
+ lo, hi = physical_interior_indices(scene, axis)
68
+ lo_coord = float(axis_coords[lo].item())
69
+ hi_coord = float(axis_coords[hi].item())
70
+ span = hi_coord - lo_coord
71
+ placement = float(fraction)
72
+ if direction_component >= 0.0:
73
+ target = lo_coord + placement * span
74
+ else:
75
+ target = hi_coord - placement * span
76
+ return int(torch.argmin(torch.abs(axis_coords - target)).item())
77
+
78
+
79
+ def soft_plane_wave_coordinate(scene, axis: str, direction_component: float, *, fraction: float = 0.05) -> float:
80
+ scene = prepare_scene(scene)
81
+ index = soft_plane_wave_index(scene, axis, direction_component, fraction=fraction)
82
+ axis_coords = {"x": scene.x, "y": scene.y, "z": scene.z}[axis]
83
+ return float(axis_coords[index].item())
84
+
85
+
86
+ def _window_is_uniform(cells: np.ndarray) -> bool:
87
+ cells = np.asarray(cells, dtype=np.float64)
88
+ d_min = float(cells.min())
89
+ d_max = float(cells.max())
90
+ return (d_max - d_min) <= _SOFT_PLANE_UNIFORM_RTOL * max(d_max, 1e-300)
91
+
92
+
93
+ def soft_plane_wave_region_spacing(scene, *, injection_axis: str, plane_index: int, direction_sign: int) -> dict:
94
+ """Per-axis primal spacing local to the soft plane-wave launch footprint.
95
+
96
+ The soft plane-wave numerical-dispersion phase correction solves the discrete
97
+ dispersion relation with one effective spacing per axis. On a grid whose
98
+ interior is uniform along an axis (to ``_SOFT_PLANE_UNIFORM_RTOL``) that axis
99
+ returns its exact minimum cell spacing, bit-for-bit the global-minimum value
100
+ the correction used before, so uniform grids are unchanged.
101
+
102
+ On a graded axis the global minimum spacing can belong to a cell far from the
103
+ source, which mis-tunes the injected wavefront's phase velocity. Instead, the
104
+ injection axis returns the spacing of the cell the wavefront is launched into
105
+ (the Yee E/H half-cell offset that governs the one-way phase match lives
106
+ there), and each tangential axis returns the mean spacing over the physical
107
+ aperture (the best single scalar for the oblique-incidence phase across the
108
+ plane).
109
+ """
110
+ scene = prepare_scene(scene)
111
+ primal = {
112
+ "x": np.asarray(scene.dx_primal64, dtype=np.float64),
113
+ "y": np.asarray(scene.dy_primal64, dtype=np.float64),
114
+ "z": np.asarray(scene.dz_primal64, dtype=np.float64),
115
+ }
116
+ deltas = {}
117
+ for axis in "xyz":
118
+ cells = primal[axis]
119
+ lo, hi = physical_interior_indices(scene, axis)
120
+ interior = cells[lo:hi] if hi > lo else cells
121
+ if _window_is_uniform(interior):
122
+ # Uniform interior: exact spacing, bit-for-bit the previous value.
123
+ deltas[axis] = float(interior.min())
124
+ elif axis == injection_axis:
125
+ # Cell straddled by the launch plane on the propagation side.
126
+ cell_index = plane_index if direction_sign > 0 else plane_index - 1
127
+ cell_index = min(max(int(cell_index), 0), len(cells) - 1)
128
+ deltas[axis] = float(cells[cell_index])
129
+ else:
130
+ deltas[axis] = float(interior.mean())
131
+ return deltas
132
+
133
+
134
+ def plane_center(scene, axis: str, plane_coordinate: float, *, device, dtype):
135
+ scene = prepare_scene(scene)
136
+ bounds = scene.domain.domain_range
137
+ center = torch.tensor(
138
+ [
139
+ 0.5 * (bounds[0] + bounds[1]),
140
+ 0.5 * (bounds[2] + bounds[3]),
141
+ 0.5 * (bounds[4] + bounds[5]),
142
+ ],
143
+ device=device,
144
+ dtype=dtype,
145
+ )
146
+ center[_AXIS_TO_INDEX[axis]] = float(plane_coordinate)
147
+ return center
148
+
149
+
150
+ def plane_wave_profile(positions, *, direction, reference_point, propagation_speed):
151
+ direction_tensor = positions.new_tensor(direction)
152
+ reference_tensor = (
153
+ reference_point.to(device=positions.device, dtype=positions.dtype)
154
+ if isinstance(reference_point, torch.Tensor)
155
+ else positions.new_tensor(reference_point)
156
+ )
157
+ delay = torch.sum((positions - reference_tensor) * direction_tensor, dim=-1) / float(propagation_speed)
158
+ amplitude = torch.ones_like(delay)
159
+ return amplitude, delay
160
+
161
+
162
+ def _astigmatic_curvature_delay(transverse, z, z_rayleigh, propagation_speed):
163
+ radius_of_curvature = torch.where(
164
+ torch.abs(z) > 1e-9,
165
+ z * (1.0 + (z_rayleigh / torch.clamp(torch.abs(z), min=1e-30)).square()),
166
+ torch.full_like(z, float("inf")),
167
+ )
168
+ return torch.where(
169
+ torch.isfinite(radius_of_curvature),
170
+ transverse.square() / (2.0 * radius_of_curvature * float(propagation_speed)),
171
+ torch.zeros_like(z),
172
+ )
173
+
174
+
175
+ def _beam_profile_core(
176
+ positions,
177
+ *,
178
+ direction,
179
+ polarization,
180
+ beam_waist_u,
181
+ beam_waist_v,
182
+ focus,
183
+ focus_offset_u,
184
+ focus_offset_v,
185
+ frequency,
186
+ propagation_speed,
187
+ ):
188
+ direction_tensor = positions.new_tensor(direction)
189
+ polarization_tensor = positions.new_tensor(polarization)
190
+ binormal_tensor = torch.cross(direction_tensor, polarization_tensor, dim=0)
191
+ focus_tensor = positions.new_tensor(focus)
192
+
193
+ rel = positions - focus_tensor
194
+ longitudinal = torch.sum(rel * direction_tensor, dim=-1)
195
+ transverse_u = torch.sum(rel * polarization_tensor, dim=-1)
196
+ transverse_v = torch.sum(rel * binormal_tensor, dim=-1)
197
+
198
+ # Per-axis longitudinal coordinate measured from each transverse-axis waist plane.
199
+ z_u = longitudinal - float(focus_offset_u)
200
+ z_v = longitudinal - float(focus_offset_v)
201
+
202
+ wavelength = float(propagation_speed) / float(frequency)
203
+ z_rayleigh_u = math.pi * float(beam_waist_u) ** 2 / max(wavelength, 1e-30)
204
+ z_rayleigh_v = math.pi * float(beam_waist_v) ** 2 / max(wavelength, 1e-30)
205
+
206
+ beam_radius_u = float(beam_waist_u) * torch.sqrt(1.0 + (z_u / max(z_rayleigh_u, 1e-30)).square())
207
+ beam_radius_v = float(beam_waist_v) * torch.sqrt(1.0 + (z_v / max(z_rayleigh_v, 1e-30)).square())
208
+
209
+ amplitude = (
210
+ torch.sqrt(float(beam_waist_u) / beam_radius_u)
211
+ * torch.sqrt(float(beam_waist_v) / beam_radius_v)
212
+ * torch.exp(
213
+ -transverse_u.square() / beam_radius_u.square()
214
+ - transverse_v.square() / beam_radius_v.square()
215
+ )
216
+ )
217
+
218
+ curvature_delay = _astigmatic_curvature_delay(
219
+ transverse_u, z_u, z_rayleigh_u, propagation_speed
220
+ ) + _astigmatic_curvature_delay(transverse_v, z_v, z_rayleigh_v, propagation_speed)
221
+ gouy_phase = 0.5 * (
222
+ torch.atan(z_u / max(z_rayleigh_u, 1e-30)) + torch.atan(z_v / max(z_rayleigh_v, 1e-30))
223
+ )
224
+ gouy_delay = gouy_phase / (2.0 * math.pi * float(frequency))
225
+ delay = longitudinal / float(propagation_speed) + curvature_delay - gouy_delay
226
+ return amplitude, delay
227
+
228
+
229
+ def gaussian_beam_profile(
230
+ positions,
231
+ *,
232
+ direction,
233
+ polarization,
234
+ beam_waist,
235
+ focus,
236
+ frequency,
237
+ propagation_speed,
238
+ ):
239
+ return _beam_profile_core(
240
+ positions,
241
+ direction=direction,
242
+ polarization=polarization,
243
+ beam_waist_u=beam_waist,
244
+ beam_waist_v=beam_waist,
245
+ focus=focus,
246
+ focus_offset_u=0.0,
247
+ focus_offset_v=0.0,
248
+ frequency=frequency,
249
+ propagation_speed=propagation_speed,
250
+ )
251
+
252
+
253
+ def astigmatic_gaussian_beam_profile(
254
+ positions,
255
+ *,
256
+ direction,
257
+ polarization,
258
+ beam_waist,
259
+ focus,
260
+ focus_offsets,
261
+ frequency,
262
+ propagation_speed,
263
+ ):
264
+ beam_waist_u, beam_waist_v = beam_waist
265
+ focus_offset_u, focus_offset_v = focus_offsets
266
+ return _beam_profile_core(
267
+ positions,
268
+ direction=direction,
269
+ polarization=polarization,
270
+ beam_waist_u=beam_waist_u,
271
+ beam_waist_v=beam_waist_v,
272
+ focus=focus,
273
+ focus_offset_u=focus_offset_u,
274
+ focus_offset_v=focus_offset_v,
275
+ frequency=frequency,
276
+ propagation_speed=propagation_speed,
277
+ )
278
+
279
+
280
+ def beam_profile_from_source(
281
+ positions,
282
+ source,
283
+ *,
284
+ frequency,
285
+ propagation_speed,
286
+ polarization=None,
287
+ ):
288
+ polarization = source["polarization"] if polarization is None else polarization
289
+ if source["kind"] == "astigmatic_gaussian_beam":
290
+ return astigmatic_gaussian_beam_profile(
291
+ positions,
292
+ direction=source["direction"],
293
+ polarization=polarization,
294
+ beam_waist=(source["beam_waist_u"], source["beam_waist_v"]),
295
+ focus=source["focus"],
296
+ focus_offsets=(source["focus_u"], source["focus_v"]),
297
+ frequency=frequency,
298
+ propagation_speed=propagation_speed,
299
+ )
300
+ return gaussian_beam_profile(
301
+ positions,
302
+ direction=source["direction"],
303
+ polarization=polarization,
304
+ beam_waist=source["beam_waist"],
305
+ focus=source["focus"],
306
+ frequency=frequency,
307
+ propagation_speed=propagation_speed,
308
+ )
309
+
310
+
311
+ class AuxiliaryGrid1D:
312
+ def __init__(
313
+ self,
314
+ *,
315
+ s_min,
316
+ s_max,
317
+ ds,
318
+ dt,
319
+ wave_speed,
320
+ impedance,
321
+ source_time,
322
+ device,
323
+ dtype,
324
+ absorber_cells=20,
325
+ source_buffer_cells=6,
326
+ fdtd_module=None,
327
+ ):
328
+ self.s_min = float(s_min)
329
+ self.s_max = float(s_max)
330
+ self.ds = float(ds)
331
+ self.dt = float(dt)
332
+ self.wave_speed = float(wave_speed)
333
+ self.impedance = float(impedance)
334
+ self.source_time = source_time
335
+ self.device = torch.device(device)
336
+ self.dtype = dtype
337
+ self.absorber_cells = max(int(absorber_cells), 0)
338
+ self.source_buffer_cells = max(int(source_buffer_cells), 0)
339
+ self.fdtd_module = fdtd_module
340
+
341
+ if self.ds <= 0.0:
342
+ raise ValueError("AuxiliaryGrid1D ds must be > 0.")
343
+ if self.dt <= 0.0:
344
+ raise ValueError("AuxiliaryGrid1D dt must be > 0.")
345
+ if self.wave_speed <= 0.0:
346
+ raise ValueError("AuxiliaryGrid1D wave_speed must be > 0.")
347
+ if self.s_max <= self.s_min:
348
+ raise ValueError("AuxiliaryGrid1D requires s_max > s_min.")
349
+
350
+ electric_cells = max(int(math.ceil((self.s_max - self.s_min) / self.ds)) + 1, 4)
351
+ self.electric = torch.zeros(electric_cells, device=self.device, dtype=dtype)
352
+ self.magnetic = torch.zeros(electric_cells - 1, device=self.device, dtype=dtype)
353
+ self.source_index = 0
354
+
355
+ self.eps = 1.0 / (self.impedance * self.wave_speed)
356
+ self.mu = self.impedance / self.wave_speed
357
+ self._build_loss_profiles()
358
+ self.time_step = 0
359
+
360
+ @property
361
+ def electric_time(self):
362
+ return self.time_step * self.dt
363
+
364
+ @property
365
+ def magnetic_time(self):
366
+ return (self.time_step - 0.5) * self.dt
367
+
368
+ def _can_use_compiled_auxiliary(self) -> bool:
369
+ return self.device.type == "cuda" and self.dtype == torch.float32
370
+
371
+ def _resolve_fdtd_module(self):
372
+ if self.fdtd_module is not None:
373
+ return self.fdtd_module
374
+ if not self._can_use_compiled_auxiliary():
375
+ return None
376
+
377
+ from ..cuda.backend import get_native_fdtd_module
378
+
379
+ self.fdtd_module = get_native_fdtd_module()
380
+ return self.fdtd_module
381
+
382
+ def _build_loss_profiles(self):
383
+ sigma_e = torch.zeros_like(self.electric)
384
+ sigma_h = torch.zeros_like(self.magnetic)
385
+ edge_cells_e = min(self.absorber_cells, max(self.electric.numel() - 1, 0))
386
+ edge_cells_h = min(self.absorber_cells, max(self.magnetic.numel(), 0))
387
+ if edge_cells_e > 0:
388
+ max_sigma = 0.35
389
+ ramp_e = ((torch.arange(edge_cells_e, device=self.device, dtype=self.dtype) + 1.0) / edge_cells_e) ** 2
390
+ sigma_e[-edge_cells_e:] = max_sigma * ramp_e
391
+ if edge_cells_h > 0:
392
+ max_sigma = 0.35
393
+ ramp_h = ((torch.arange(edge_cells_h, device=self.device, dtype=self.dtype) + 0.5) / edge_cells_h) ** 2
394
+ sigma_h[-edge_cells_h:] = max_sigma * ramp_h
395
+
396
+ sigma_factor_e = 0.5 * self.dt * sigma_e / max(self.eps, 1e-30)
397
+ sigma_factor_h = 0.5 * self.dt * sigma_h / max(self.mu, 1e-30)
398
+ self.electric_decay = (1.0 - sigma_factor_e) / (1.0 + sigma_factor_e)
399
+ self.electric_curl = (self.dt / (self.eps * self.ds)) / (1.0 + sigma_factor_e)
400
+ self.magnetic_decay = (1.0 - sigma_factor_h) / (1.0 + sigma_factor_h)
401
+ self.magnetic_curl = (self.dt / (self.mu * self.ds)) / (1.0 + sigma_factor_h)
402
+
403
+ def _interpolate(self, field, *, origin, positions, out=None):
404
+ positions_tensor = torch.as_tensor(positions, device=self.device, dtype=self.dtype)
405
+ coord = torch.clamp((positions_tensor - float(origin)) / self.ds, min=0.0, max=field.numel() - 1.0)
406
+ lower = torch.floor(coord).to(torch.int64)
407
+ upper = torch.clamp(lower + 1, max=field.numel() - 1)
408
+ frac = coord - lower.to(self.dtype)
409
+ if out is None:
410
+ out = torch.empty_like(positions_tensor)
411
+ out.copy_(field[lower])
412
+ out.lerp_(field[upper], frac)
413
+ return out
414
+
415
+ def sample_e(self, positions, out=None):
416
+ return self._interpolate(self.electric, origin=self.s_min, positions=positions, out=out)
417
+
418
+ def sample_h(self, positions, out=None):
419
+ return self._interpolate(self.magnetic, origin=self.s_min + 0.5 * self.ds, positions=positions, out=out)
420
+
421
+ def _apply_source(self):
422
+ self.electric[self.source_index] = evaluate_source_time(self.source_time, self.electric_time)
423
+
424
+ def _advance_magnetic_compiled(self):
425
+ self._resolve_fdtd_module().updateAuxiliaryMagnetic1D(
426
+ Magnetic=self.magnetic,
427
+ Electric=self.electric,
428
+ MagneticDecay=self.magnetic_decay,
429
+ MagneticCurl=self.magnetic_curl,
430
+ ).launchRaw()
431
+
432
+ def _advance_electric_compiled(self):
433
+ source_value = float(evaluate_source_time(self.source_time, self.electric_time))
434
+ self._resolve_fdtd_module().updateAuxiliaryElectric1D(
435
+ Electric=self.electric,
436
+ Magnetic=self.magnetic,
437
+ ElectricDecay=self.electric_decay,
438
+ ElectricCurl=self.electric_curl,
439
+ sourceIndex=int(self.source_index),
440
+ sourceValue=source_value,
441
+ ).launchRaw()
442
+
443
+ def advance_magnetic(self):
444
+ if self._can_use_compiled_auxiliary():
445
+ self._advance_magnetic_compiled()
446
+ return
447
+ if self.magnetic.numel() > 0:
448
+ curl_e = self.electric[1:] - self.electric[:-1]
449
+ self.magnetic = self.magnetic_decay * self.magnetic - self.magnetic_curl * curl_e
450
+
451
+ def advance_electric(self):
452
+ if self._can_use_compiled_auxiliary():
453
+ self._advance_electric_compiled()
454
+ self.time_step += 1
455
+ return
456
+ if self.electric.numel() > 2:
457
+ curl_h = self.magnetic[1:] - self.magnetic[:-1]
458
+ self.electric[1:-1] = self.electric_decay[1:-1] * self.electric[1:-1] - self.electric_curl[1:-1] * curl_h
459
+
460
+ self._apply_source()
461
+ self.electric[-1] = 0.0
462
+ self.time_step += 1
463
+
464
+ def advance(self):
465
+ self.advance_magnetic()
466
+ self.advance_electric()
467
+
468
+ def step(self):
469
+ self.advance()