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,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)
|