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,335 @@
1
+ """Adaptive (auto) mesh generation for nonuniform FDTD Yee grids.
2
+
3
+ Pure host-side float64 meshing. Per axis, every structure / override AABB face
4
+ becomes a hard cell boundary; each face-bounded interval receives a target step
5
+ ``wavelength / (n_max * min_steps_per_wavelength)`` (further reduced by mesh
6
+ overrides and layer refinement); intervals are then filled with uniform or
7
+ geometrically graded cells so adjacent cells never differ by more than
8
+ ``max_ratio`` anywhere on the axis.
9
+
10
+ Nodes are absolute float64 coordinates, so the snapping / target / ratio
11
+ invariants hold up to rounding that scales with ``ulp(|coordinate|) / cell``;
12
+ cells within a few orders of magnitude of the coordinate ULP degrade.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import math
18
+
19
+ import numpy as np
20
+
21
+ _C0 = 299_792_458.0
22
+ # Structure AABBs come from float32 ``to_mesh`` vertices, whose relative
23
+ # precision is ~1.2e-7. A face nominally on a domain boundary (or on another
24
+ # face) can therefore land that far away from it. Snap faces closer than this
25
+ # together so a float32-rounded coordinate never spawns a sub-cell sliver
26
+ # interval, which the ratio fixpoint would otherwise resolve into a fan of
27
+ # near-zero-width cells (crushing the Courant limit).
28
+ _FACE_SNAP_REL = 1e-6
29
+ _SLACK = 1.0 + 1e-12
30
+ _MAX_AXIS_CELLS = 10_000_000
31
+
32
+
33
+ def _axis_tolerance(lo: float, hi: float) -> float:
34
+ """Face-snapping tolerance for one axis.
35
+
36
+ The relative term covers domains near the origin; the float32-ULP term
37
+ covers domains offset far from the origin, where the absolute face rounding
38
+ grows with coordinate magnitude. The ULP term is a few ULP (not scaled by
39
+ the span/offset ratio) so a large offset never swallows real structure
40
+ faces inside a much smaller domain span.
41
+ """
42
+ ulp = float(np.spacing(np.float32(max(abs(lo), abs(hi)))))
43
+ return max(_FACE_SNAP_REL * (hi - lo), 4.0 * ulp)
44
+
45
+
46
+ def _collect_boundaries(lo: float, hi: float, faces) -> np.ndarray:
47
+ tolerance = _axis_tolerance(lo, hi)
48
+ interior = sorted({min(max(float(face), lo), hi) for face in faces})
49
+ boundaries = [lo]
50
+ for value in interior:
51
+ if value - boundaries[-1] > tolerance and hi - value > tolerance:
52
+ boundaries.append(value)
53
+ boundaries.append(hi)
54
+ return np.asarray(boundaries, dtype=np.float64)
55
+
56
+
57
+ def _interval_targets(boundaries, base_dl, constraints, layer_min_cells) -> np.ndarray:
58
+ lengths = np.diff(boundaries)
59
+ targets = np.full(lengths.size, float(base_dl), dtype=np.float64)
60
+ tolerance = _axis_tolerance(float(boundaries[0]), float(boundaries[-1]))
61
+ for c_lo, c_hi, c_dl in constraints:
62
+ overlap = (boundaries[:-1] < c_hi - tolerance) & (boundaries[1:] > c_lo + tolerance)
63
+ targets[overlap] = np.minimum(targets[overlap], c_dl)
64
+ if layer_min_cells is not None:
65
+ thin = lengths < layer_min_cells * targets
66
+ targets[thin] = lengths[thin] / layer_min_cells
67
+ return targets
68
+
69
+
70
+ def _uniform_fixpoint(lengths, targets, max_ratio):
71
+ """Per-interval uniform cell sizes with cross-boundary ratio <= max_ratio.
72
+
73
+ Fixpoint of ``u_i = L_i / ceil(L_i / min(t_i, r*u_{i-1}, r*u_{i+1}))``:
74
+ every interval is filled exactly, its uniform step never exceeds the
75
+ interval target, and adjacent intervals differ by at most ``max_ratio``.
76
+ """
77
+ counts = np.maximum(1, np.ceil(lengths / targets - 1e-12).astype(np.int64))
78
+ sizes = lengths / counts
79
+ changed = True
80
+ while changed:
81
+ changed = False
82
+ for i in range(sizes.size):
83
+ allowed = targets[i]
84
+ if i > 0:
85
+ allowed = min(allowed, max_ratio * sizes[i - 1])
86
+ if i + 1 < sizes.size:
87
+ allowed = min(allowed, max_ratio * sizes[i + 1])
88
+ if sizes[i] > allowed * _SLACK:
89
+ counts[i] = max(counts[i] + 1, int(math.ceil(lengths[i] / allowed - 1e-12)))
90
+ sizes[i] = lengths[i] / counts[i]
91
+ changed = True
92
+ return sizes, counts
93
+
94
+
95
+ def _graded_sizes(length, cap, edge_lo, edge_hi, max_ratio):
96
+ """Graded fill: exact edge cells, geometric ramps at rate ``max_ratio``,
97
+ and a remainder-absorbing plateau in ``[cap/max_ratio, cap]``.
98
+
99
+ ``edge_lo`` / ``edge_hi`` pin the boundary-adjacent cell size (``None`` at
100
+ a free domain edge). When the interval is too short to ramp all the way up
101
+ to ``cap``, the plateau cap is lowered geometrically until a valid profile
102
+ fits; every reduced-cap profile preserves the target and ratio invariants.
103
+ Returns ``None`` only when no plateau above the pinned edges fits, in which
104
+ case the uniform fill is already within a few cells of optimal.
105
+ """
106
+ edges = [edge for edge in (edge_lo, edge_hi) if edge is not None]
107
+ floor = max(edges) if edges else 0.0
108
+ while cap > floor:
109
+ ramp_lo, ramp_hi = [], []
110
+ for edge, ramp in ((edge_lo, ramp_lo), (edge_hi, ramp_hi)):
111
+ if edge is None:
112
+ continue
113
+ ramp.append(edge)
114
+ value = edge * max_ratio
115
+ while value < cap * (1.0 - 1e-12):
116
+ ramp.append(value)
117
+ value *= max_ratio
118
+ middle = length - sum(ramp_lo) - sum(ramp_hi)
119
+ if middle >= cap / (max_ratio - 1.0):
120
+ plateau_count = max(1, int(math.ceil(middle / cap - 1e-12)))
121
+ plateau = middle / plateau_count
122
+ return np.asarray(
123
+ ramp_lo + [plateau] * plateau_count + ramp_hi[::-1], dtype=np.float64
124
+ )
125
+ cap /= max_ratio
126
+ return None
127
+
128
+
129
+ def _fill_axis(boundaries, targets, max_ratio) -> np.ndarray:
130
+ lengths = np.diff(boundaries)
131
+ sizes, counts = _uniform_fixpoint(lengths, targets, max_ratio)
132
+ nodes = [np.asarray([boundaries[0]])]
133
+ for i in range(lengths.size):
134
+ edge_lo = min(sizes[i], sizes[i - 1]) if i > 0 else None
135
+ edge_hi = min(sizes[i], sizes[i + 1]) if i + 1 < lengths.size else None
136
+ cells = _graded_sizes(lengths[i], targets[i], edge_lo, edge_hi, max_ratio)
137
+ if cells is None or cells.size >= counts[i]:
138
+ cells = np.full(counts[i], sizes[i])
139
+ # Distribute float64 summation drift across the profile instead of
140
+ # letting the exact-boundary snap dump it all into the last cell.
141
+ cells = cells * (lengths[i] / cells.sum())
142
+ nodes.append(boundaries[i] + np.cumsum(cells[:-1]))
143
+ nodes.append(np.asarray([boundaries[i + 1]]))
144
+ return np.concatenate(nodes)
145
+
146
+
147
+ def mesh_axis(
148
+ lo,
149
+ hi,
150
+ *,
151
+ wavelength,
152
+ min_steps_per_wavelength,
153
+ max_ratio,
154
+ index_regions=(),
155
+ override_regions=(),
156
+ layer_min_cells=None,
157
+ pml_cells=(0, 0),
158
+ ) -> np.ndarray:
159
+ """Mesh one axis of the domain ``[lo, hi]`` into float64 node coordinates.
160
+
161
+ ``index_regions`` are ``(lo, hi, refractive_index)`` structure extents and
162
+ ``override_regions`` are ``(lo, hi, max_dl)`` mesh-override extents; the
163
+ faces of both snap to cell boundaries. ``pml_cells`` gives the CPML
164
+ thickness in cells at the low / high domain edge; those bands are
165
+ uniformized piecewise between hard faces (see
166
+ ``_uniformize_boundary_band``).
167
+ """
168
+ lo, hi = float(lo), float(hi)
169
+ if hi <= lo:
170
+ raise ValueError("mesh_axis requires hi > lo.")
171
+ base_dl = float(wavelength) / float(min_steps_per_wavelength)
172
+ faces = []
173
+ constraints = []
174
+ for r_lo, r_hi, index in index_regions:
175
+ faces += [r_lo, r_hi]
176
+ constraints.append(
177
+ (float(r_lo), float(r_hi), base_dl / max(float(index), 1.0))
178
+ )
179
+ for r_lo, r_hi, dl in override_regions:
180
+ faces += [r_lo, r_hi]
181
+ constraints.append((float(r_lo), float(r_hi), float(dl)))
182
+ boundaries = _collect_boundaries(lo, hi, faces)
183
+ targets = _interval_targets(boundaries, base_dl, constraints, layer_min_cells)
184
+ estimate = float(np.ceil(np.diff(boundaries) / targets).sum())
185
+ if not math.isfinite(estimate) or estimate > _MAX_AXIS_CELLS:
186
+ raise ValueError(
187
+ f"GridSpec.auto meshing of [{lo:g}, {hi:g}] implies ~{estimate:.3g} cells "
188
+ f"on one axis (limit {_MAX_AXIS_CELLS:.0e}); check the wavelength, "
189
+ "min_steps_per_wavelength, and override dl units."
190
+ )
191
+ nodes = _fill_axis(boundaries, targets, float(max_ratio))
192
+ return _uniformize_boundary_band(nodes, boundaries, *pml_cells)
193
+
194
+
195
+ def _geometry_aabb(geometry):
196
+ try:
197
+ vertices, _ = geometry.to_mesh()
198
+ except NotImplementedError as error:
199
+ raise ValueError(
200
+ "GridSpec.auto requires an axis-aligned bounding box for every geometry; "
201
+ f"{type(geometry).__name__}.to_mesh() is not implemented."
202
+ ) from error
203
+ points = vertices.detach().cpu().numpy().astype(np.float64)
204
+ return points.min(axis=0), points.max(axis=0)
205
+
206
+
207
+ def _max_real_component(value) -> float:
208
+ if hasattr(value, "as_tuple"):
209
+ return float(max(value.as_tuple()))
210
+ return float(np.real(value))
211
+
212
+
213
+ def _refractive_index(material, frequency: float) -> float:
214
+ try:
215
+ sample = material.evaluate_at_frequency(frequency)
216
+ except (AttributeError, NotImplementedError):
217
+ sample = material.evaluate_static() if hasattr(material, "evaluate_static") else material
218
+ eps = max(_max_real_component(getattr(sample, "eps_r", 1.0)), 1.0)
219
+ mu = max(_max_real_component(getattr(sample, "mu_r", 1.0)), 1.0)
220
+ return math.sqrt(eps * mu)
221
+
222
+
223
+ def _meshing_wavelength(scene) -> float:
224
+ if scene.grid.wavelength is not None:
225
+ return float(scene.grid.wavelength)
226
+ frequencies = []
227
+ for source in scene.resolved_sources():
228
+ source_time = getattr(source, "source_time", None)
229
+ # Broadband waveforms (GaussianPulse, RickerWavelet) carry significant
230
+ # spectral content above their center frequency; mesh for the same
231
+ # characteristic frequency that auto_dt() uses.
232
+ frequency = getattr(source_time, "characteristic_frequency", None)
233
+ if frequency is None:
234
+ frequency = getattr(source_time, "frequency", None)
235
+ if frequency is not None:
236
+ frequencies.append(float(frequency))
237
+ if not frequencies:
238
+ raise ValueError(
239
+ "GridSpec.auto could not derive a meshing wavelength: no scene source "
240
+ "carries a source_time frequency. Pass GridSpec.auto(wavelength=...) or "
241
+ "give the sources a frequency-bearing source_time."
242
+ )
243
+ return _C0 / max(frequencies)
244
+
245
+
246
+ def _uniformize_boundary_band(
247
+ nodes: np.ndarray, boundaries: np.ndarray, n_low: int, n_high: int
248
+ ) -> np.ndarray:
249
+ """Uniformize the outermost ``n_low`` / ``n_high`` cells piecewise.
250
+
251
+ The CPML absorber occupies the outermost ``pml_thickness`` cells of the
252
+ domain and grades by physical depth, so a uniform absorber band gives the
253
+ cleanest absorption. Each band is re-spaced piecewise: hard interval
254
+ boundaries (structure / override faces) inside the band stay fixed, and the
255
+ nodes of each sub-segment between them are spread uniformly. Face snapping
256
+ is therefore preserved, and so are the per-interval targets: every replaced
257
+ cell already satisfied its interval target, so the sub-segment mean does
258
+ too. The domain edges and the first interior node past each band stay
259
+ fixed, so the interior mesh is untouched. It is a no-op where the band is
260
+ already uniform (the common vacuum-edge case). Bands are skipped (rather
261
+ than overlapped) on domains too thin to hold both plus an interior cell.
262
+ """
263
+ cells = nodes.size - 1
264
+ n_low = max(0, int(n_low))
265
+ n_high = max(0, int(n_high))
266
+ if n_low + n_high == 0 or n_low + n_high >= cells:
267
+ return nodes
268
+ out = nodes.copy()
269
+ # Interval boundaries are exact node values by construction (_fill_axis
270
+ # appends them verbatim), so exact-match lookup is safe.
271
+ positions = np.searchsorted(nodes, boundaries)
272
+ hard = {int(i) for i, b in zip(positions, boundaries) if nodes[i] == b}
273
+ for start, stop in ((0, n_low), (cells - n_high, cells)):
274
+ anchors = [start] + sorted(i for i in hard if start < i < stop) + [stop]
275
+ for a, b in zip(anchors, anchors[1:]):
276
+ out[a : b + 1] = np.linspace(out[a], out[b], b - a + 1)
277
+ return out
278
+
279
+
280
+ def resolve_auto_grid(scene):
281
+ """Materialize a ``GridSpec.auto`` scene grid into per-axis node arrays.
282
+
283
+ Returns float64 ``(x_nodes, y_nodes, z_nodes)`` spanning the scene domain
284
+ exactly, suitable for ``GridSpec.custom``. The outermost cells under each
285
+ PML face are uniformized between structure faces so the absorber sees a
286
+ clean, constant-step band without losing face snapping or refinement.
287
+ """
288
+ grid = scene.grid
289
+ wavelength = _meshing_wavelength(scene)
290
+ frequency = _C0 / wavelength
291
+ index_regions = []
292
+ for structure in scene.structures:
293
+ if not getattr(structure, "enabled", True):
294
+ continue
295
+ aabb_lo, aabb_hi = _geometry_aabb(structure.geometry)
296
+ index_regions.append(
297
+ (aabb_lo, aabb_hi, _refractive_index(structure.material, frequency))
298
+ )
299
+ for region in scene.material_regions:
300
+ aabb_lo, aabb_hi = _geometry_aabb(region.geometry)
301
+ index = math.sqrt(
302
+ max(max(region.eps_bounds), 1.0) * max(max(region.mu_bounds), 1.0)
303
+ )
304
+ index_regions.append((aabb_lo, aabb_hi, index))
305
+ override_regions = []
306
+ for override in grid.override_structures:
307
+ aabb_lo, aabb_hi = _geometry_aabb(override.geometry)
308
+ override_regions.append((aabb_lo, aabb_hi, override.dl))
309
+ layer = grid.layer_refinement
310
+ nodes = []
311
+ for axis_index, (axis_lo, axis_hi) in enumerate(scene.domain.bounds):
312
+ axis = "xyz"[axis_index]
313
+ layer_min_cells = (
314
+ layer.min_cells if layer is not None and layer.covers(axis) else None
315
+ )
316
+ axis_nodes = mesh_axis(
317
+ axis_lo,
318
+ axis_hi,
319
+ wavelength=wavelength,
320
+ min_steps_per_wavelength=grid.min_steps_per_wavelength,
321
+ max_ratio=grid.max_ratio,
322
+ index_regions=[
323
+ (lo[axis_index], hi[axis_index], index) for lo, hi, index in index_regions
324
+ ],
325
+ override_regions=[
326
+ (lo[axis_index], hi[axis_index], dl[axis_index])
327
+ for lo, hi, dl in override_regions
328
+ ],
329
+ layer_min_cells=layer_min_cells,
330
+ # Domain is physical. PreparedScene appends the PML cells outside
331
+ # these nodes, so the physical auto mesh has no internal PML band.
332
+ pml_cells=(0, 0),
333
+ )
334
+ nodes.append(axis_nodes)
335
+ return tuple(nodes)