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,1102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import torch
|
|
7
|
+
|
|
8
|
+
from ...sources import POINT_DIPOLE_REFERENCE_WIDTH
|
|
9
|
+
from .spatial import (
|
|
10
|
+
beam_profile_from_source,
|
|
11
|
+
plane_center,
|
|
12
|
+
plane_wave_profile,
|
|
13
|
+
resolve_injection_axis,
|
|
14
|
+
soft_plane_wave_index,
|
|
15
|
+
soft_plane_wave_region_spacing,
|
|
16
|
+
source_plane_index,
|
|
17
|
+
)
|
|
18
|
+
from .modes import sample_mode_source_component, solve_mode_source_profile
|
|
19
|
+
from .temporal import append_source_term, apply_compiled_source_terms, apply_generic_source_terms
|
|
20
|
+
from .tfsf_common import build_term_from_profile, build_terms_from_specs, slice_coeff_patch, solve_numerical_wavenumber
|
|
21
|
+
from .tfsf_specs import E_CURL_ATTR, H_CURL_ATTR, build_discrete_tfsf_specs, magnetic_physical_vector
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_FACE_SPEC_RANGES = {
|
|
25
|
+
("x", "low"): slice(0, 2),
|
|
26
|
+
("x", "high"): slice(2, 4),
|
|
27
|
+
("y", "low"): slice(4, 6),
|
|
28
|
+
("y", "high"): slice(6, 8),
|
|
29
|
+
("z", "low"): slice(8, 10),
|
|
30
|
+
("z", "high"): slice(10, 12),
|
|
31
|
+
}
|
|
32
|
+
_AXIS_TO_INDEX = {"x": 0, "y": 1, "z": 2}
|
|
33
|
+
_ETA0 = 376.730313668
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _normalized_point_dipole_profile(
|
|
37
|
+
dist_sq: torch.Tensor, width: float, control_volumes: torch.Tensor
|
|
38
|
+
) -> torch.Tensor:
|
|
39
|
+
width_sq = 2.0 * float(width) ** 2
|
|
40
|
+
profile = torch.exp(-dist_sq / width_sq)
|
|
41
|
+
integrated_mass = torch.sum(profile * control_volumes)
|
|
42
|
+
return profile / torch.clamp(integrated_mass, min=torch.finfo(profile.dtype).eps)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _sample_control_widths(coords: torch.Tensor) -> torch.Tensor:
|
|
46
|
+
count = int(coords.numel())
|
|
47
|
+
if count <= 1:
|
|
48
|
+
return torch.ones_like(coords)
|
|
49
|
+
diffs = coords[1:] - coords[:-1]
|
|
50
|
+
widths = torch.empty_like(coords)
|
|
51
|
+
widths[0] = 0.5 * diffs[0]
|
|
52
|
+
widths[-1] = 0.5 * diffs[-1]
|
|
53
|
+
if count > 2:
|
|
54
|
+
widths[1:-1] = 0.5 * (diffs[:-1] + diffs[1:])
|
|
55
|
+
return widths
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _yee_component_control_volumes(solver, field_name: str) -> torch.Tensor:
|
|
59
|
+
coords = _yee_component_coords(solver, field_name)
|
|
60
|
+
widths = tuple(_sample_control_widths(axis) for axis in coords)
|
|
61
|
+
return widths[0][:, None, None] * widths[1][None, :, None] * widths[2][None, None, :]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _yee_component_coords(solver, field_name: str) -> tuple[torch.Tensor, ...]:
|
|
65
|
+
scene = solver.scene
|
|
66
|
+
coords = {
|
|
67
|
+
"Ex": (scene.x_half, scene.y, scene.z),
|
|
68
|
+
"Ey": (scene.x, scene.y_half, scene.z),
|
|
69
|
+
"Ez": (scene.x, scene.y, scene.z_half),
|
|
70
|
+
}[field_name]
|
|
71
|
+
return tuple(axis.to(device=solver.device, dtype=solver.Ex.dtype) for axis in coords)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _symmetry_plane_source_scale(scene, field_name: str, position) -> float:
|
|
75
|
+
"""Scale a source that lies exactly on an image plane to its full-grid weight."""
|
|
76
|
+
field_axis = {"Ex": 0, "Ey": 1, "Ez": 2}[field_name]
|
|
77
|
+
scale = 1.0
|
|
78
|
+
for axis, symmetry in enumerate(scene.symmetry):
|
|
79
|
+
if symmetry is None:
|
|
80
|
+
continue
|
|
81
|
+
_mode, face = symmetry
|
|
82
|
+
plane = float(scene.domain.bounds[axis][0 if face == "low" else 1])
|
|
83
|
+
span = float(scene.domain.bounds[axis][1] - scene.domain.bounds[axis][0])
|
|
84
|
+
if abs(float(position[axis]) - plane) > 1e-9 * max(span, 1.0):
|
|
85
|
+
continue
|
|
86
|
+
# A tangential E component is sampled on the image plane: the half-cell
|
|
87
|
+
# control volume is half of its full-grid counterpart. A normal component
|
|
88
|
+
# is staggered off the plane as well, so the full-grid source interpolation
|
|
89
|
+
# also splits equally between the two mirrored edges.
|
|
90
|
+
scale *= 0.25 if field_axis == axis else 0.5
|
|
91
|
+
return scale
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _axis_control_overlap(coords: torch.Tensor, lo: float, hi: float) -> torch.Tensor:
|
|
95
|
+
"""Fraction of every sample's dual control interval covered by ``[lo, hi]``."""
|
|
96
|
+
count = int(coords.numel())
|
|
97
|
+
if count <= 1:
|
|
98
|
+
return torch.ones_like(coords) if lo <= float(coords[0]) <= hi else torch.zeros_like(coords)
|
|
99
|
+
midpoints = 0.5 * (coords[:-1] + coords[1:])
|
|
100
|
+
left = torch.cat((coords[:1] - 0.5 * (coords[1:2] - coords[:1]), midpoints))
|
|
101
|
+
right = torch.cat((midpoints, coords[-1:] + 0.5 * (coords[-1:] - coords[-2:-1])))
|
|
102
|
+
overlap = torch.clamp(
|
|
103
|
+
torch.minimum(right, torch.as_tensor(hi, device=coords.device, dtype=coords.dtype))
|
|
104
|
+
- torch.maximum(left, torch.as_tensor(lo, device=coords.device, dtype=coords.dtype)),
|
|
105
|
+
min=0.0,
|
|
106
|
+
)
|
|
107
|
+
return overlap / (right - left)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _yee_box_overlap(solver, field_name: str, lo, hi):
|
|
111
|
+
"""Return a compact Yee patch containing volume-averaged box indicator weights."""
|
|
112
|
+
axis_weights = tuple(
|
|
113
|
+
_axis_control_overlap(coords, float(lo[axis]), float(hi[axis]))
|
|
114
|
+
for axis, coords in enumerate(_yee_component_coords(solver, field_name))
|
|
115
|
+
)
|
|
116
|
+
nonzero = tuple(torch.nonzero(weights > 0.0, as_tuple=False).flatten() for weights in axis_weights)
|
|
117
|
+
if any(int(indices.numel()) == 0 for indices in nonzero):
|
|
118
|
+
return None
|
|
119
|
+
start = tuple(int(indices[0].item()) for indices in nonzero)
|
|
120
|
+
stop = tuple(int(indices[-1].item()) + 1 for indices in nonzero)
|
|
121
|
+
slices = tuple(axis_weights[axis][start[axis] : stop[axis]] for axis in range(3))
|
|
122
|
+
weights = slices[0][:, None, None] * slices[1][None, :, None] * slices[2][None, None, :]
|
|
123
|
+
return start, stop, weights
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _ideal_axis_weights(coords: torch.Tensor, position: float) -> tuple[list[int], list[float]]:
|
|
127
|
+
if coords.ndim != 1 or coords.numel() == 0:
|
|
128
|
+
raise ValueError("coords must be a non-empty 1D tensor.")
|
|
129
|
+
if coords.numel() == 1:
|
|
130
|
+
return [0], [1.0]
|
|
131
|
+
|
|
132
|
+
probe = coords.new_tensor(float(position))
|
|
133
|
+
upper = int(torch.searchsorted(coords, probe, right=False).item())
|
|
134
|
+
if upper <= 0:
|
|
135
|
+
return [0], [1.0]
|
|
136
|
+
if upper >= coords.numel():
|
|
137
|
+
return [int(coords.numel() - 1)], [1.0]
|
|
138
|
+
|
|
139
|
+
lower = upper - 1
|
|
140
|
+
lower_coord = float(coords[lower].item())
|
|
141
|
+
upper_coord = float(coords[upper].item())
|
|
142
|
+
span = upper_coord - lower_coord
|
|
143
|
+
if span <= torch.finfo(coords.dtype).eps:
|
|
144
|
+
return [lower], [1.0]
|
|
145
|
+
|
|
146
|
+
fraction = (float(position) - lower_coord) / span
|
|
147
|
+
if fraction <= 1e-12:
|
|
148
|
+
return [lower], [1.0]
|
|
149
|
+
if fraction >= 1.0 - 1e-12:
|
|
150
|
+
return [upper], [1.0]
|
|
151
|
+
return [lower, upper], [1.0 - fraction, fraction]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _ideal_point_dipole_term(
|
|
155
|
+
px: torch.Tensor,
|
|
156
|
+
py: torch.Tensor,
|
|
157
|
+
pz: torch.Tensor,
|
|
158
|
+
source_position,
|
|
159
|
+
*,
|
|
160
|
+
eps_tensor: torch.Tensor,
|
|
161
|
+
offsets,
|
|
162
|
+
dt: float,
|
|
163
|
+
polarization_component: float,
|
|
164
|
+
control_volumes: torch.Tensor,
|
|
165
|
+
):
|
|
166
|
+
x_indices, x_weights = _ideal_axis_weights(px, source_position[0])
|
|
167
|
+
y_indices, y_weights = _ideal_axis_weights(py, source_position[1])
|
|
168
|
+
z_indices, z_weights = _ideal_axis_weights(pz, source_position[2])
|
|
169
|
+
|
|
170
|
+
local_start = (x_indices[0], y_indices[0], z_indices[0])
|
|
171
|
+
local_stop = (x_indices[-1] + 1, y_indices[-1] + 1, z_indices[-1] + 1)
|
|
172
|
+
global_offsets = (
|
|
173
|
+
offsets[0] + local_start[0],
|
|
174
|
+
offsets[1] + local_start[1],
|
|
175
|
+
offsets[2] + local_start[2],
|
|
176
|
+
)
|
|
177
|
+
eps_slice = eps_tensor[
|
|
178
|
+
global_offsets[0] : offsets[0] + local_stop[0],
|
|
179
|
+
global_offsets[1] : offsets[1] + local_stop[1],
|
|
180
|
+
global_offsets[2] : offsets[2] + local_stop[2],
|
|
181
|
+
]
|
|
182
|
+
volume_slice = control_volumes[
|
|
183
|
+
global_offsets[0] : offsets[0] + local_stop[0],
|
|
184
|
+
global_offsets[1] : offsets[1] + local_stop[1],
|
|
185
|
+
global_offsets[2] : offsets[2] + local_stop[2],
|
|
186
|
+
]
|
|
187
|
+
source_patch = torch.zeros_like(eps_slice)
|
|
188
|
+
source_scale = -float(dt) * float(polarization_component)
|
|
189
|
+
|
|
190
|
+
for ix, wx in zip(x_indices, x_weights):
|
|
191
|
+
for iy, wy in zip(y_indices, y_weights):
|
|
192
|
+
for iz, wz in zip(z_indices, z_weights):
|
|
193
|
+
local_index = (ix - local_start[0], iy - local_start[1], iz - local_start[2])
|
|
194
|
+
source_patch[local_index] += (
|
|
195
|
+
source_scale
|
|
196
|
+
* float(wx)
|
|
197
|
+
* float(wy)
|
|
198
|
+
* float(wz)
|
|
199
|
+
/ (eps_slice[local_index] * volume_slice[local_index])
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
return global_offsets, source_patch
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _axis_index_window(nodes64, lo_value, hi_value, size_cap):
|
|
206
|
+
"""Node-index window [lo, hi) covering [lo_value, hi_value] on an axis."""
|
|
207
|
+
if float(hi_value) < float(nodes64[0]) or float(lo_value) > float(nodes64[-1]):
|
|
208
|
+
return 0, 0
|
|
209
|
+
lo_index = max(0, int(np.searchsorted(nodes64, float(lo_value), side="right")) - 1)
|
|
210
|
+
hi_index = min(int(size_cap), int(np.searchsorted(nodes64, float(hi_value), side="left")) + 1)
|
|
211
|
+
return lo_index, hi_index
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _prepare_point_dipole_source(solver, source, *, source_index):
|
|
215
|
+
scene = solver.scene
|
|
216
|
+
width = float(source["width"])
|
|
217
|
+
polarization = source["polarization"]
|
|
218
|
+
profile_kind = source.get("profile", "gaussian")
|
|
219
|
+
source_time = source["source_time"]
|
|
220
|
+
source_omega = 2.0 * np.pi * float(source_time["frequency"])
|
|
221
|
+
if profile_kind == "ideal":
|
|
222
|
+
cutoff = 3.0 * POINT_DIPOLE_REFERENCE_WIDTH
|
|
223
|
+
else:
|
|
224
|
+
cutoff = 3.0 * max(width, 0.5 * POINT_DIPOLE_REFERENCE_WIDTH)
|
|
225
|
+
coord_dtype = solver.Ex.dtype
|
|
226
|
+
control_volumes = {
|
|
227
|
+
field_name: _yee_component_control_volumes(solver, field_name)
|
|
228
|
+
for field_name, component in zip(("Ex", "Ey", "Ez"), polarization)
|
|
229
|
+
if not np.isclose(component, 0.0)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for image_position, phase_real, phase_imag in solver._iter_source_images(source["position"], cutoff):
|
|
233
|
+
src_x, src_y, src_z = image_position
|
|
234
|
+
ix_start, ix_end = _axis_index_window(scene.x_nodes64, src_x - cutoff, src_x + cutoff, solver.Nx)
|
|
235
|
+
iy_start, iy_end = _axis_index_window(scene.y_nodes64, src_y - cutoff, src_y + cutoff, solver.Ny)
|
|
236
|
+
iz_start, iz_end = _axis_index_window(scene.z_nodes64, src_z - cutoff, src_z + cutoff, solver.Nz)
|
|
237
|
+
|
|
238
|
+
if ix_end <= ix_start or iy_end <= iy_start or iz_end <= iz_start:
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
if polarization[2] != 0:
|
|
242
|
+
iz_end_ez = min(iz_end, solver.Nz - 1)
|
|
243
|
+
if iz_end_ez > iz_start:
|
|
244
|
+
px = scene.x[ix_start:ix_end].to(dtype=coord_dtype)
|
|
245
|
+
py = scene.y[iy_start:iy_end].to(dtype=coord_dtype)
|
|
246
|
+
pz = scene.z_half[iz_start:iz_end_ez].to(dtype=coord_dtype)
|
|
247
|
+
dist_sq = (
|
|
248
|
+
(px[:, None, None] - src_x) ** 2
|
|
249
|
+
+ (py[None, :, None] - src_y) ** 2
|
|
250
|
+
+ (pz[None, None, :] - src_z) ** 2
|
|
251
|
+
)
|
|
252
|
+
if profile_kind == "ideal":
|
|
253
|
+
patch_offsets, source_patch = _ideal_point_dipole_term(
|
|
254
|
+
px,
|
|
255
|
+
py,
|
|
256
|
+
pz,
|
|
257
|
+
image_position,
|
|
258
|
+
eps_tensor=solver.eps_Ez,
|
|
259
|
+
offsets=(ix_start, iy_start, iz_start),
|
|
260
|
+
dt=solver.dt,
|
|
261
|
+
polarization_component=polarization[2],
|
|
262
|
+
control_volumes=control_volumes["Ez"],
|
|
263
|
+
)
|
|
264
|
+
else:
|
|
265
|
+
volume_slice = control_volumes["Ez"][
|
|
266
|
+
ix_start:ix_end, iy_start:iy_end, iz_start:iz_end_ez
|
|
267
|
+
]
|
|
268
|
+
profile = _normalized_point_dipole_profile(dist_sq, width, volume_slice)
|
|
269
|
+
source_patch = (
|
|
270
|
+
(-solver.dt * polarization[2] / solver.eps_Ez[ix_start:ix_end, iy_start:iy_end, iz_start:iz_end_ez])
|
|
271
|
+
* profile
|
|
272
|
+
)
|
|
273
|
+
patch_offsets = (ix_start, iy_start, iz_start)
|
|
274
|
+
source_patch *= _symmetry_plane_source_scale(scene, "Ez", image_position)
|
|
275
|
+
append_source_term(
|
|
276
|
+
solver._source_terms,
|
|
277
|
+
solver,
|
|
278
|
+
field_name="Ez",
|
|
279
|
+
offsets=patch_offsets,
|
|
280
|
+
patch=source_patch,
|
|
281
|
+
phase_real=phase_real,
|
|
282
|
+
phase_imag=phase_imag,
|
|
283
|
+
source_index=source_index,
|
|
284
|
+
source_time=source_time,
|
|
285
|
+
omega=source_omega,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
if polarization[0] != 0:
|
|
289
|
+
ix_end_ex = min(ix_end, solver.Nx - 1)
|
|
290
|
+
if ix_end_ex > ix_start:
|
|
291
|
+
px = scene.x_half[ix_start:ix_end_ex].to(dtype=coord_dtype)
|
|
292
|
+
py = scene.y[iy_start:iy_end].to(dtype=coord_dtype)
|
|
293
|
+
pz = scene.z[iz_start:iz_end].to(dtype=coord_dtype)
|
|
294
|
+
dist_sq = (
|
|
295
|
+
(px[:, None, None] - src_x) ** 2
|
|
296
|
+
+ (py[None, :, None] - src_y) ** 2
|
|
297
|
+
+ (pz[None, None, :] - src_z) ** 2
|
|
298
|
+
)
|
|
299
|
+
if profile_kind == "ideal":
|
|
300
|
+
patch_offsets, source_patch = _ideal_point_dipole_term(
|
|
301
|
+
px,
|
|
302
|
+
py,
|
|
303
|
+
pz,
|
|
304
|
+
image_position,
|
|
305
|
+
eps_tensor=solver.eps_Ex,
|
|
306
|
+
offsets=(ix_start, iy_start, iz_start),
|
|
307
|
+
dt=solver.dt,
|
|
308
|
+
polarization_component=polarization[0],
|
|
309
|
+
control_volumes=control_volumes["Ex"],
|
|
310
|
+
)
|
|
311
|
+
else:
|
|
312
|
+
volume_slice = control_volumes["Ex"][
|
|
313
|
+
ix_start:ix_end_ex, iy_start:iy_end, iz_start:iz_end
|
|
314
|
+
]
|
|
315
|
+
profile = _normalized_point_dipole_profile(dist_sq, width, volume_slice)
|
|
316
|
+
source_patch = (
|
|
317
|
+
(-solver.dt * polarization[0] / solver.eps_Ex[ix_start:ix_end_ex, iy_start:iy_end, iz_start:iz_end])
|
|
318
|
+
* profile
|
|
319
|
+
)
|
|
320
|
+
patch_offsets = (ix_start, iy_start, iz_start)
|
|
321
|
+
source_patch *= _symmetry_plane_source_scale(scene, "Ex", image_position)
|
|
322
|
+
append_source_term(
|
|
323
|
+
solver._source_terms,
|
|
324
|
+
solver,
|
|
325
|
+
field_name="Ex",
|
|
326
|
+
offsets=patch_offsets,
|
|
327
|
+
patch=source_patch,
|
|
328
|
+
phase_real=phase_real,
|
|
329
|
+
phase_imag=phase_imag,
|
|
330
|
+
source_index=source_index,
|
|
331
|
+
source_time=source_time,
|
|
332
|
+
omega=source_omega,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if polarization[1] != 0:
|
|
336
|
+
iy_end_ey = min(iy_end, solver.Ny - 1)
|
|
337
|
+
if iy_end_ey > iy_start:
|
|
338
|
+
px = scene.x[ix_start:ix_end].to(dtype=coord_dtype)
|
|
339
|
+
py = scene.y_half[iy_start:iy_end_ey].to(dtype=coord_dtype)
|
|
340
|
+
pz = scene.z[iz_start:iz_end].to(dtype=coord_dtype)
|
|
341
|
+
dist_sq = (
|
|
342
|
+
(px[:, None, None] - src_x) ** 2
|
|
343
|
+
+ (py[None, :, None] - src_y) ** 2
|
|
344
|
+
+ (pz[None, None, :] - src_z) ** 2
|
|
345
|
+
)
|
|
346
|
+
if profile_kind == "ideal":
|
|
347
|
+
patch_offsets, source_patch = _ideal_point_dipole_term(
|
|
348
|
+
px,
|
|
349
|
+
py,
|
|
350
|
+
pz,
|
|
351
|
+
image_position,
|
|
352
|
+
eps_tensor=solver.eps_Ey,
|
|
353
|
+
offsets=(ix_start, iy_start, iz_start),
|
|
354
|
+
dt=solver.dt,
|
|
355
|
+
polarization_component=polarization[1],
|
|
356
|
+
control_volumes=control_volumes["Ey"],
|
|
357
|
+
)
|
|
358
|
+
else:
|
|
359
|
+
volume_slice = control_volumes["Ey"][
|
|
360
|
+
ix_start:ix_end, iy_start:iy_end_ey, iz_start:iz_end
|
|
361
|
+
]
|
|
362
|
+
profile = _normalized_point_dipole_profile(dist_sq, width, volume_slice)
|
|
363
|
+
source_patch = (
|
|
364
|
+
(-solver.dt * polarization[1] / solver.eps_Ey[ix_start:ix_end, iy_start:iy_end_ey, iz_start:iz_end])
|
|
365
|
+
* profile
|
|
366
|
+
)
|
|
367
|
+
patch_offsets = (ix_start, iy_start, iz_start)
|
|
368
|
+
source_patch *= _symmetry_plane_source_scale(scene, "Ey", image_position)
|
|
369
|
+
append_source_term(
|
|
370
|
+
solver._source_terms,
|
|
371
|
+
solver,
|
|
372
|
+
field_name="Ey",
|
|
373
|
+
offsets=patch_offsets,
|
|
374
|
+
patch=source_patch,
|
|
375
|
+
phase_real=phase_real,
|
|
376
|
+
phase_imag=phase_imag,
|
|
377
|
+
source_index=source_index,
|
|
378
|
+
source_time=source_time,
|
|
379
|
+
omega=source_omega,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
_UNIFORM_CURRENT_COMPONENTS = (("Ex", 0), ("Ey", 1), ("Ez", 2))
|
|
384
|
+
_CURRENT_ELECTRIC_MAP = {"Jx": "Ex", "Jy": "Ey", "Jz": "Ez"}
|
|
385
|
+
_CURRENT_MAGNETIC_MAP = {"Mx": "Hx", "My": "Hy", "Mz": "Hz"}
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _region_index_range(solver, field_name, lo, hi):
|
|
389
|
+
scene = solver.scene
|
|
390
|
+
axis_nodes = (scene.x_nodes64, scene.y_nodes64, scene.z_nodes64)
|
|
391
|
+
axis_half = (scene.x_half64, scene.y_half64, scene.z_half64)
|
|
392
|
+
sizes = tuple(int(dim) for dim in getattr(solver, field_name).shape)
|
|
393
|
+
half_axes = solver._COMPONENT_HALF_OFFSET_AXES[field_name]
|
|
394
|
+
start = []
|
|
395
|
+
stop = []
|
|
396
|
+
for axis in range(3):
|
|
397
|
+
if float(lo[axis]) == float(hi[axis]):
|
|
398
|
+
component_coords = axis_half[axis] if half_axes[axis] else axis_nodes[axis]
|
|
399
|
+
component_coords = component_coords[: sizes[axis]]
|
|
400
|
+
nearest = int(np.argmin(np.abs(component_coords - float(lo[axis]))))
|
|
401
|
+
start.append(nearest)
|
|
402
|
+
stop.append(nearest + 1)
|
|
403
|
+
continue
|
|
404
|
+
lo_index, hi_index = _axis_index_window(axis_nodes[axis], lo[axis], hi[axis], sizes[axis])
|
|
405
|
+
if half_axes[axis]:
|
|
406
|
+
# ``_axis_index_window`` returns the bounding-node window. A Yee
|
|
407
|
+
# component centered between nodes has one fewer sample inside the
|
|
408
|
+
# same discretized box along that axis.
|
|
409
|
+
hi_index = max(lo_index, hi_index - 1)
|
|
410
|
+
start.append(lo_index)
|
|
411
|
+
stop.append(hi_index)
|
|
412
|
+
return tuple(start), tuple(stop)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _prepare_uniform_current_source(solver, source, *, source_index):
|
|
416
|
+
center = source["center"]
|
|
417
|
+
size = source["size"]
|
|
418
|
+
lo = tuple(center[axis] - 0.5 * size[axis] for axis in range(3))
|
|
419
|
+
hi = tuple(center[axis] + 0.5 * size[axis] for axis in range(3))
|
|
420
|
+
source_time = source["source_time"]
|
|
421
|
+
source_omega = 2.0 * np.pi * float(source_time["frequency"])
|
|
422
|
+
polarization = source["polarization"]
|
|
423
|
+
|
|
424
|
+
for field_name, axis in _UNIFORM_CURRENT_COMPONENTS:
|
|
425
|
+
pol_component = float(polarization[axis])
|
|
426
|
+
if np.isclose(pol_component, 0.0):
|
|
427
|
+
continue
|
|
428
|
+
overlap = _yee_box_overlap(solver, field_name, lo, hi)
|
|
429
|
+
if overlap is None:
|
|
430
|
+
continue
|
|
431
|
+
start, stop, weights = overlap
|
|
432
|
+
field_eps = getattr(solver, f"eps_{field_name}")
|
|
433
|
+
eps_slice = field_eps[start[0]:stop[0], start[1]:stop[1], start[2]:stop[2]]
|
|
434
|
+
source_patch = (-solver.dt * pol_component / eps_slice) * weights
|
|
435
|
+
append_source_term(
|
|
436
|
+
solver._source_terms,
|
|
437
|
+
solver,
|
|
438
|
+
field_name=field_name,
|
|
439
|
+
offsets=start,
|
|
440
|
+
patch=source_patch,
|
|
441
|
+
source_index=source_index,
|
|
442
|
+
source_time=source_time,
|
|
443
|
+
omega=source_omega,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _axis_interp_weights(coords, query):
|
|
448
|
+
count = int(coords.numel())
|
|
449
|
+
if count == 1:
|
|
450
|
+
index = torch.zeros_like(query, dtype=torch.long)
|
|
451
|
+
return index, index, torch.zeros_like(query)
|
|
452
|
+
upper = torch.searchsorted(coords, query.contiguous(), right=True).clamp(1, count - 1)
|
|
453
|
+
lower = upper - 1
|
|
454
|
+
lower_coord = coords[lower]
|
|
455
|
+
upper_coord = coords[upper]
|
|
456
|
+
fraction = (query - lower_coord) / (upper_coord - lower_coord)
|
|
457
|
+
return lower, upper, fraction.clamp(0.0, 1.0)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _trilinear_sample(values, axes, positions):
|
|
461
|
+
x = positions[..., 0]
|
|
462
|
+
y = positions[..., 1]
|
|
463
|
+
z = positions[..., 2]
|
|
464
|
+
ix0, ix1, wx = _axis_interp_weights(axes[0], x)
|
|
465
|
+
iy0, iy1, wy = _axis_interp_weights(axes[1], y)
|
|
466
|
+
iz0, iz1, wz = _axis_interp_weights(axes[2], z)
|
|
467
|
+
|
|
468
|
+
def gather(i, j, k):
|
|
469
|
+
return values[i, j, k]
|
|
470
|
+
|
|
471
|
+
c00 = gather(ix0, iy0, iz0) * (1.0 - wz) + gather(ix0, iy0, iz1) * wz
|
|
472
|
+
c01 = gather(ix0, iy1, iz0) * (1.0 - wz) + gather(ix0, iy1, iz1) * wz
|
|
473
|
+
c10 = gather(ix1, iy0, iz0) * (1.0 - wz) + gather(ix1, iy0, iz1) * wz
|
|
474
|
+
c11 = gather(ix1, iy1, iz0) * (1.0 - wz) + gather(ix1, iy1, iz1) * wz
|
|
475
|
+
c0 = c00 * (1.0 - wy) + c01 * wy
|
|
476
|
+
c1 = c10 * (1.0 - wy) + c11 * wy
|
|
477
|
+
return c0 * (1.0 - wx) + c1 * wx
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _dataset_axes_tensors(solver, dataset):
|
|
481
|
+
return tuple(
|
|
482
|
+
torch.tensor(axis, device=solver.device, dtype=solver.Ex.dtype)
|
|
483
|
+
for axis in dataset.coords
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _dataset_region_bounds(dataset):
|
|
488
|
+
lo = tuple(float(axis[0]) for axis in dataset.coords)
|
|
489
|
+
hi = tuple(float(axis[-1]) for axis in dataset.coords)
|
|
490
|
+
return lo, hi
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _append_interpolated_current(
|
|
494
|
+
solver,
|
|
495
|
+
*,
|
|
496
|
+
field_name,
|
|
497
|
+
values,
|
|
498
|
+
axes,
|
|
499
|
+
denom_tensor,
|
|
500
|
+
region_lo,
|
|
501
|
+
region_hi,
|
|
502
|
+
source_time,
|
|
503
|
+
source_omega,
|
|
504
|
+
source_index,
|
|
505
|
+
term_list,
|
|
506
|
+
):
|
|
507
|
+
start, stop = _region_index_range(solver, field_name, region_lo, region_hi)
|
|
508
|
+
if any(stop[a] <= start[a] for a in range(3)):
|
|
509
|
+
return
|
|
510
|
+
shape = tuple(stop[a] - start[a] for a in range(3))
|
|
511
|
+
positions = solver._component_positions(field_name, start, shape, dtype=solver.Ex.dtype)
|
|
512
|
+
sampled = _trilinear_sample(values, axes, positions)
|
|
513
|
+
if torch.max(torch.abs(sampled)).item() <= 1e-30:
|
|
514
|
+
return
|
|
515
|
+
denom_slice = denom_tensor[start[0]:stop[0], start[1]:stop[1], start[2]:stop[2]]
|
|
516
|
+
source_patch = (-solver.dt / denom_slice) * sampled
|
|
517
|
+
append_source_term(
|
|
518
|
+
term_list,
|
|
519
|
+
solver,
|
|
520
|
+
field_name=field_name,
|
|
521
|
+
offsets=start,
|
|
522
|
+
patch=source_patch,
|
|
523
|
+
source_index=source_index,
|
|
524
|
+
source_time=source_time,
|
|
525
|
+
omega=source_omega,
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _prepare_custom_current_source(solver, source, *, source_index):
|
|
530
|
+
dataset = source["dataset"]
|
|
531
|
+
source_time = source["source_time"]
|
|
532
|
+
source_omega = 2.0 * np.pi * float(source_time["frequency"])
|
|
533
|
+
axes = _dataset_axes_tensors(solver, dataset)
|
|
534
|
+
region_lo, region_hi = _dataset_region_bounds(dataset)
|
|
535
|
+
|
|
536
|
+
for key, array in dataset.components.items():
|
|
537
|
+
values = torch.tensor(array, device=solver.device, dtype=solver.Ex.dtype)
|
|
538
|
+
if key in _CURRENT_ELECTRIC_MAP:
|
|
539
|
+
field_name = _CURRENT_ELECTRIC_MAP[key]
|
|
540
|
+
denom_tensor = getattr(solver, f"eps_{field_name}")
|
|
541
|
+
term_list = solver._source_terms
|
|
542
|
+
else:
|
|
543
|
+
field_name = _CURRENT_MAGNETIC_MAP[key]
|
|
544
|
+
denom_tensor = getattr(solver, f"mu_{field_name}")
|
|
545
|
+
term_list = solver._magnetic_source_terms
|
|
546
|
+
_append_interpolated_current(
|
|
547
|
+
solver,
|
|
548
|
+
field_name=field_name,
|
|
549
|
+
values=values,
|
|
550
|
+
axes=axes,
|
|
551
|
+
denom_tensor=denom_tensor,
|
|
552
|
+
region_lo=region_lo,
|
|
553
|
+
region_hi=region_hi,
|
|
554
|
+
source_time=source_time,
|
|
555
|
+
source_omega=source_omega,
|
|
556
|
+
source_index=source_index,
|
|
557
|
+
term_list=term_list,
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _prepare_custom_field_source(solver, source, *, source_index):
|
|
562
|
+
dataset = source["dataset"]
|
|
563
|
+
normal_axis = source["normal_axis"]
|
|
564
|
+
source_time = source["source_time"]
|
|
565
|
+
source_omega = 2.0 * np.pi * float(source_time["frequency"])
|
|
566
|
+
axes = _dataset_axes_tensors(solver, dataset)
|
|
567
|
+
region_lo, region_hi = _dataset_region_bounds(dataset)
|
|
568
|
+
axis_index = _AXIS_TO_INDEX[normal_axis]
|
|
569
|
+
plane_coord = 0.5 * (float(region_lo[axis_index]) + float(region_hi[axis_index]))
|
|
570
|
+
plane_coords = getattr(solver.scene, normal_axis)
|
|
571
|
+
plane_index = int(torch.argmin(torch.abs(plane_coords - plane_coord)).item())
|
|
572
|
+
|
|
573
|
+
lower = [0, 0, 0]
|
|
574
|
+
upper = [0, 0, 0]
|
|
575
|
+
for index, axis in enumerate("xyz"):
|
|
576
|
+
if axis == normal_axis:
|
|
577
|
+
lower[index] = plane_index
|
|
578
|
+
upper[index] = plane_index + 1
|
|
579
|
+
continue
|
|
580
|
+
nodes64 = (solver.scene.x_nodes64, solver.scene.y_nodes64, solver.scene.z_nodes64)[index]
|
|
581
|
+
size_cap = (solver.Nx, solver.Ny, solver.Nz)[index]
|
|
582
|
+
start, stop = _axis_index_window(
|
|
583
|
+
nodes64,
|
|
584
|
+
region_lo[index],
|
|
585
|
+
region_hi[index],
|
|
586
|
+
size_cap,
|
|
587
|
+
)
|
|
588
|
+
lower[index] = start
|
|
589
|
+
upper[index] = stop - 1
|
|
590
|
+
|
|
591
|
+
electric_specs, magnetic_specs = build_discrete_tfsf_specs(tuple(lower), tuple(upper))
|
|
592
|
+
face_side = "low"
|
|
593
|
+
electric_specs = electric_specs[_FACE_SPEC_RANGES[(normal_axis, face_side)]]
|
|
594
|
+
magnetic_specs = magnetic_specs[_FACE_SPEC_RANGES[(normal_axis, face_side)]]
|
|
595
|
+
component_values = {
|
|
596
|
+
name: torch.tensor(values, device=solver.device, dtype=solver.Ex.dtype)
|
|
597
|
+
for name, values in dataset.components.items()
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
def build_profile_term(spec, coeff_patch, _component_scale):
|
|
601
|
+
values = component_values[spec["incident_name"]]
|
|
602
|
+
positions = _surface_plane_spec_positions(solver, spec)
|
|
603
|
+
sampled = _trilinear_sample(values, axes, positions)
|
|
604
|
+
if spec["incident_name"].startswith("H"):
|
|
605
|
+
# The discrete face specification stores incident magnetic fields
|
|
606
|
+
# with the update-equation sign (``-k x E``), while FieldDataset
|
|
607
|
+
# exposes the physical ``H = k x E / eta`` convention.
|
|
608
|
+
sampled = -sampled
|
|
609
|
+
scale = spec["sign"] * sampled * coeff_patch
|
|
610
|
+
return build_term_from_profile(
|
|
611
|
+
solver,
|
|
612
|
+
field_name=spec["field_name"],
|
|
613
|
+
offsets=spec["offsets"],
|
|
614
|
+
scale=scale,
|
|
615
|
+
delay_patch=torch.zeros_like(scale),
|
|
616
|
+
activation_delay_patch=None,
|
|
617
|
+
source_time=source_time,
|
|
618
|
+
omega=source_omega,
|
|
619
|
+
source_index=source_index,
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
solver._electric_source_terms.extend(build_terms_from_specs(
|
|
623
|
+
solver,
|
|
624
|
+
electric_specs,
|
|
625
|
+
{name: (1.0 if name in component_values else 0.0) for name in ("Hx", "Hy", "Hz")},
|
|
626
|
+
E_CURL_ATTR,
|
|
627
|
+
build_profile_term,
|
|
628
|
+
))
|
|
629
|
+
solver._magnetic_source_terms.extend(build_terms_from_specs(
|
|
630
|
+
solver,
|
|
631
|
+
magnetic_specs,
|
|
632
|
+
{name: (1.0 if name in component_values else 0.0) for name in ("Ex", "Ey", "Ez")},
|
|
633
|
+
H_CURL_ATTR,
|
|
634
|
+
build_profile_term,
|
|
635
|
+
))
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _prepare_surface_source(solver, source, *, source_index):
|
|
639
|
+
if solver.scene.boundary.uses_kind("periodic") or solver.scene.boundary.uses_kind("bloch"):
|
|
640
|
+
raise NotImplementedError(
|
|
641
|
+
f"{source['kind']} currently supports only none, pml, pec, or pmc boundaries."
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
source_time = source["source_time"]
|
|
645
|
+
source_frequency = float(source_time["frequency"])
|
|
646
|
+
source_omega = 2.0 * np.pi * source_frequency
|
|
647
|
+
direction = source["direction"]
|
|
648
|
+
axis = resolve_injection_axis(direction, source.get("injection_axis"))
|
|
649
|
+
axis_index = {"x": 0, "y": 1, "z": 2}[axis]
|
|
650
|
+
plane_index = source_plane_index(solver.scene, axis, float(direction[axis_index]))
|
|
651
|
+
plane_coordinate = solver._plane_coordinate(axis, plane_index)
|
|
652
|
+
reference_point = plane_center(
|
|
653
|
+
solver.scene,
|
|
654
|
+
axis,
|
|
655
|
+
plane_coordinate,
|
|
656
|
+
device=solver.device,
|
|
657
|
+
dtype=solver.Ex.dtype,
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
for field_name, polarization_component in zip(("Ex", "Ey", "Ez"), source["polarization"]):
|
|
661
|
+
if np.isclose(polarization_component, 0.0):
|
|
662
|
+
continue
|
|
663
|
+
offsets, shape = solver._component_plane_spec(field_name, axis, plane_index)
|
|
664
|
+
positions = solver._component_positions(field_name, offsets, shape, dtype=solver.Ex.dtype)
|
|
665
|
+
if source["kind"] == "plane_wave":
|
|
666
|
+
spatial_amplitude, delay_patch = plane_wave_profile(
|
|
667
|
+
positions,
|
|
668
|
+
direction=direction,
|
|
669
|
+
reference_point=reference_point,
|
|
670
|
+
propagation_speed=solver.c,
|
|
671
|
+
)
|
|
672
|
+
else:
|
|
673
|
+
spatial_amplitude, delay_patch = beam_profile_from_source(
|
|
674
|
+
positions,
|
|
675
|
+
source,
|
|
676
|
+
frequency=source_frequency,
|
|
677
|
+
propagation_speed=solver.c,
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
field_eps = getattr(solver, f"eps_{field_name}")
|
|
681
|
+
eps_slice = field_eps[
|
|
682
|
+
offsets[0] : offsets[0] + shape[0],
|
|
683
|
+
offsets[1] : offsets[1] + shape[1],
|
|
684
|
+
offsets[2] : offsets[2] + shape[2],
|
|
685
|
+
]
|
|
686
|
+
source_patch = (-solver.dt * polarization_component / eps_slice) * spatial_amplitude
|
|
687
|
+
if source_time["kind"] == "cw":
|
|
688
|
+
phase_shift = source_omega * delay_patch
|
|
689
|
+
append_source_term(
|
|
690
|
+
solver._source_terms,
|
|
691
|
+
solver,
|
|
692
|
+
field_name=field_name,
|
|
693
|
+
offsets=offsets,
|
|
694
|
+
cw_cos_patch=source_patch * torch.cos(phase_shift),
|
|
695
|
+
cw_sin_patch=source_patch * torch.sin(phase_shift),
|
|
696
|
+
source_index=source_index,
|
|
697
|
+
source_time=source_time,
|
|
698
|
+
omega=source_omega,
|
|
699
|
+
)
|
|
700
|
+
continue
|
|
701
|
+
append_source_term(
|
|
702
|
+
solver._source_terms,
|
|
703
|
+
solver,
|
|
704
|
+
field_name=field_name,
|
|
705
|
+
offsets=offsets,
|
|
706
|
+
patch=source_patch,
|
|
707
|
+
delay_patch=delay_patch,
|
|
708
|
+
source_index=source_index,
|
|
709
|
+
source_time=source_time,
|
|
710
|
+
omega=source_omega,
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def _surface_plane_spec_positions(solver, spec):
|
|
715
|
+
return solver._component_positions(
|
|
716
|
+
spec["incident_name"],
|
|
717
|
+
spec["sample_offsets"],
|
|
718
|
+
spec["sample_shape"],
|
|
719
|
+
dtype=solver.Ex.dtype,
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _plane_wave_power_scale(source, aperture_bounds, injection_axis: str) -> float:
|
|
724
|
+
"""Absolute surface-current scale for unit time-averaged incident power.
|
|
725
|
+
|
|
726
|
+
The soft ``PlaneWave`` face injects the surface-equivalent currents of the
|
|
727
|
+
target incident wave (``J_s = n x H_inc`` on the electric face, ``M_s =
|
|
728
|
+
-n x E_inc`` on the magnetic face). By the surface-equivalence principle
|
|
729
|
+
these radiate the incident field forward with unit gain, so the injected
|
|
730
|
+
incident amplitude equals the specified ``E`` amplitude scaled here. On the
|
|
731
|
+
Yee grid the numerical wave impedance of a plane wave that satisfies the
|
|
732
|
+
discrete dispersion relation is exactly ``eta0`` -- the leapfrog identity
|
|
733
|
+
``sin(omega*dt/2)/(c*dt) = sin(k~*d/2)/d`` makes ``H0/E0 = 1/eta0`` -- so the
|
|
734
|
+
physical-impedance magnetic current the injector uses (``magnetic_physical_
|
|
735
|
+
vector(..., eta0)``) is the correct numerical amplitude and the forward
|
|
736
|
+
radiation gain is unity. No empirical calibration is therefore required.
|
|
737
|
+
|
|
738
|
+
Normalizing to unit incident power over the illuminated aperture, the
|
|
739
|
+
time-averaged forward power ``0.5*|E0|^2*A*cos(theta)/eta0`` equals one when
|
|
740
|
+
``|E0| = 1/sqrt(unit_power)`` with ``unit_power = A*cos(theta)/(2*eta0)``.
|
|
741
|
+
The residual discrete-injection error is below the 2% absolute-power
|
|
742
|
+
acceptance across frequencies and spacings (see
|
|
743
|
+
``tests/sources/incident/test_soft_planewave_absolute_power.py``).
|
|
744
|
+
"""
|
|
745
|
+
axis_index = _AXIS_TO_INDEX[injection_axis]
|
|
746
|
+
tangential_extents = [
|
|
747
|
+
float(axis_bounds[1] - axis_bounds[0])
|
|
748
|
+
for index, axis_bounds in enumerate(aperture_bounds)
|
|
749
|
+
if index != axis_index
|
|
750
|
+
]
|
|
751
|
+
aperture_area = tangential_extents[0] * tangential_extents[1]
|
|
752
|
+
incidence_cosine = abs(float(source["direction"][axis_index]))
|
|
753
|
+
unit_power = aperture_area * incidence_cosine / (2.0 * _ETA0)
|
|
754
|
+
if unit_power <= 0.0:
|
|
755
|
+
raise ValueError("PlaneWave source requires a positive aperture power for normalization.")
|
|
756
|
+
return 1.0 / np.sqrt(unit_power)
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def _beam_power_scale(source, injection_axis: str) -> float:
|
|
760
|
+
"""Peak electric-field scale for a unit-power Gaussian beam.
|
|
761
|
+
|
|
762
|
+
The astigmatic profile uses ``exp(-u^2 / wu^2 - v^2 / wv^2)`` with the
|
|
763
|
+
longitudinal prefactor that preserves its transverse L2 mass. Therefore
|
|
764
|
+
``integral |E/E0|^2 dA = pi*wu*wv/2`` at every propagation plane.
|
|
765
|
+
"""
|
|
766
|
+
if source["kind"] == "astigmatic_gaussian_beam":
|
|
767
|
+
waist_u = float(source["beam_waist_u"])
|
|
768
|
+
waist_v = float(source["beam_waist_v"])
|
|
769
|
+
else:
|
|
770
|
+
waist_u = waist_v = float(source["beam_waist"])
|
|
771
|
+
axis_index = _AXIS_TO_INDEX[injection_axis]
|
|
772
|
+
incidence_cosine = abs(float(source["direction"][axis_index]))
|
|
773
|
+
unit_power = math.pi * waist_u * waist_v * incidence_cosine / (4.0 * _ETA0)
|
|
774
|
+
if unit_power <= 0.0:
|
|
775
|
+
raise ValueError("Gaussian beam requires a positive transverse power integral.")
|
|
776
|
+
return 1.0 / math.sqrt(unit_power)
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def _validate_soft_surface_source_boundary(boundary, direction, injection_axis: str) -> None:
|
|
780
|
+
if boundary.uses_kind("bloch"):
|
|
781
|
+
raise NotImplementedError(
|
|
782
|
+
"PlaneWave soft injection does not support Bloch boundaries; use a TFSF source."
|
|
783
|
+
)
|
|
784
|
+
incompatible_periodic_axes = [
|
|
785
|
+
axis
|
|
786
|
+
for index, axis in enumerate("xyz")
|
|
787
|
+
if boundary.axis_kind(axis) == "periodic"
|
|
788
|
+
and (axis == injection_axis or abs(float(direction[index])) > 1.0e-12)
|
|
789
|
+
]
|
|
790
|
+
if incompatible_periodic_axes:
|
|
791
|
+
axes = ", ".join(incompatible_periodic_axes)
|
|
792
|
+
raise NotImplementedError(
|
|
793
|
+
"PlaneWave soft injection supports periodic boundaries only on zero-phase "
|
|
794
|
+
f"transverse axes; incompatible axes: {axes}."
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _prepare_power_normalized_surface_source(solver, source, *, source_index):
|
|
799
|
+
source_time = source["source_time"]
|
|
800
|
+
source_frequency = float(source_time["frequency"])
|
|
801
|
+
source_omega = 2.0 * np.pi * source_frequency
|
|
802
|
+
direction = source["direction"]
|
|
803
|
+
injection_axis = resolve_injection_axis(direction, source.get("injection_axis"))
|
|
804
|
+
_validate_soft_surface_source_boundary(
|
|
805
|
+
solver.scene.boundary,
|
|
806
|
+
direction,
|
|
807
|
+
injection_axis,
|
|
808
|
+
)
|
|
809
|
+
axis_index = _AXIS_TO_INDEX[injection_axis]
|
|
810
|
+
direction_sign = 1 if float(direction[axis_index]) >= 0.0 else -1
|
|
811
|
+
plane_index = soft_plane_wave_index(solver.scene, injection_axis, float(direction[axis_index]))
|
|
812
|
+
|
|
813
|
+
lower = []
|
|
814
|
+
upper = []
|
|
815
|
+
aperture_bounds = []
|
|
816
|
+
computational_range = solver.scene.domain_range
|
|
817
|
+
for axis in "xyz":
|
|
818
|
+
if axis == injection_axis:
|
|
819
|
+
if direction_sign > 0:
|
|
820
|
+
lo = plane_index
|
|
821
|
+
hi = plane_index + 1
|
|
822
|
+
else:
|
|
823
|
+
lo = plane_index - 1
|
|
824
|
+
hi = plane_index
|
|
825
|
+
axis_coords = {"x": solver.scene.x, "y": solver.scene.y, "z": solver.scene.z}[axis]
|
|
826
|
+
plane_coord = float(axis_coords[plane_index].item())
|
|
827
|
+
aperture_bounds.append((plane_coord, plane_coord))
|
|
828
|
+
else:
|
|
829
|
+
lo = 0
|
|
830
|
+
hi = {"x": solver.Nx, "y": solver.Ny, "z": solver.Nz}[axis] - 1
|
|
831
|
+
range_offset = 2 * _AXIS_TO_INDEX[axis]
|
|
832
|
+
aperture_bounds.append(
|
|
833
|
+
(
|
|
834
|
+
float(computational_range[range_offset]),
|
|
835
|
+
float(computational_range[range_offset + 1]),
|
|
836
|
+
)
|
|
837
|
+
)
|
|
838
|
+
lower.append(int(lo))
|
|
839
|
+
upper.append(int(hi))
|
|
840
|
+
|
|
841
|
+
lower = tuple(lower)
|
|
842
|
+
upper = tuple(upper)
|
|
843
|
+
face_side = "low" if direction_sign > 0 else "high"
|
|
844
|
+
electric_specs, magnetic_specs = build_discrete_tfsf_specs(lower, upper)
|
|
845
|
+
electric_specs = electric_specs[_FACE_SPEC_RANGES[(injection_axis, face_side)]]
|
|
846
|
+
magnetic_specs = magnetic_specs[_FACE_SPEC_RANGES[(injection_axis, face_side)]]
|
|
847
|
+
|
|
848
|
+
phase_speed = solver.c
|
|
849
|
+
# Numerical-dispersion phase correction with the grid spacing local to the
|
|
850
|
+
# launch plane and aperture. On a uniform grid this is exactly the per-axis
|
|
851
|
+
# spacing (bit-for-bit the previous global-minimum value); on a graded grid it
|
|
852
|
+
# tracks the launch-region cells the wavefront actually crosses instead of the
|
|
853
|
+
# global-minimum spacing, whose finest cell can lie far from the source and
|
|
854
|
+
# mis-tune the injected phase velocity.
|
|
855
|
+
deltas = soft_plane_wave_region_spacing(
|
|
856
|
+
solver.scene,
|
|
857
|
+
injection_axis=injection_axis,
|
|
858
|
+
plane_index=plane_index,
|
|
859
|
+
direction_sign=direction_sign,
|
|
860
|
+
)
|
|
861
|
+
k_numeric = solve_numerical_wavenumber(solver, direction, deltas)
|
|
862
|
+
if k_numeric > 1e-12:
|
|
863
|
+
phase_speed = source_omega / k_numeric
|
|
864
|
+
|
|
865
|
+
if source["kind"] == "plane_wave":
|
|
866
|
+
power_scale = _plane_wave_power_scale(source, tuple(aperture_bounds), injection_axis)
|
|
867
|
+
else:
|
|
868
|
+
power_scale = _beam_power_scale(source, injection_axis)
|
|
869
|
+
# A native Yee-plane Poynting monitor co-locates the tangential E/H pair by
|
|
870
|
+
# interpolating across their half-cell separation along the injection axis.
|
|
871
|
+
# For the injected discrete plane wave this reduces the measured normal power
|
|
872
|
+
# by cos(k_axis * delta_axis / 2). Normalize by that derived factor so a unit
|
|
873
|
+
# PlaneWave carries one watt through the full computational aperture, matching
|
|
874
|
+
# Tidy3D's infinite-plane source convention without an empirical multiplier.
|
|
875
|
+
k_axis = k_numeric * float(direction[axis_index])
|
|
876
|
+
discrete_power_factor = math.cos(0.5 * k_axis * float(deltas[injection_axis]))
|
|
877
|
+
if discrete_power_factor <= 0.0:
|
|
878
|
+
raise ValueError(
|
|
879
|
+
"PlaneWave grid is too coarse for positive discrete normal power; "
|
|
880
|
+
"refine the injection-axis spacing."
|
|
881
|
+
)
|
|
882
|
+
power_scale /= math.sqrt(discrete_power_factor)
|
|
883
|
+
electric_vector = {
|
|
884
|
+
"Ex": power_scale * float(source["polarization"][0]),
|
|
885
|
+
"Ey": power_scale * float(source["polarization"][1]),
|
|
886
|
+
"Ez": power_scale * float(source["polarization"][2]),
|
|
887
|
+
}
|
|
888
|
+
magnetic_vector = {
|
|
889
|
+
name: power_scale * value
|
|
890
|
+
for name, value in magnetic_physical_vector(direction, source["polarization"], _ETA0).items()
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
plane_coordinate = solver._plane_coordinate(injection_axis, plane_index)
|
|
894
|
+
reference_point = plane_center(
|
|
895
|
+
solver.scene,
|
|
896
|
+
injection_axis,
|
|
897
|
+
plane_coordinate,
|
|
898
|
+
device=solver.device,
|
|
899
|
+
dtype=solver.Ex.dtype,
|
|
900
|
+
)
|
|
901
|
+
beam_reference_delay = None
|
|
902
|
+
if source["kind"] != "plane_wave":
|
|
903
|
+
_, beam_reference_delay = beam_profile_from_source(
|
|
904
|
+
reference_point.reshape(1, 1, 1, 3),
|
|
905
|
+
source,
|
|
906
|
+
frequency=source_frequency,
|
|
907
|
+
propagation_speed=phase_speed,
|
|
908
|
+
)
|
|
909
|
+
beam_reference_delay = beam_reference_delay.reshape(())
|
|
910
|
+
|
|
911
|
+
def build_profile_term(spec, coeff_patch, component_scale):
|
|
912
|
+
positions = _surface_plane_spec_positions(solver, spec)
|
|
913
|
+
if source["kind"] == "plane_wave":
|
|
914
|
+
spatial_amplitude, delay_patch = plane_wave_profile(
|
|
915
|
+
positions,
|
|
916
|
+
direction=direction,
|
|
917
|
+
reference_point=reference_point,
|
|
918
|
+
propagation_speed=phase_speed,
|
|
919
|
+
)
|
|
920
|
+
else:
|
|
921
|
+
spatial_amplitude, delay_patch = beam_profile_from_source(
|
|
922
|
+
positions,
|
|
923
|
+
source,
|
|
924
|
+
frequency=source_frequency,
|
|
925
|
+
propagation_speed=phase_speed,
|
|
926
|
+
)
|
|
927
|
+
# The analytic beam phase is expressed relative to its waist. A
|
|
928
|
+
# soft source, however, starts on this launch plane: retaining the
|
|
929
|
+
# large negative waist-to-plane delay would place most of a pulsed
|
|
930
|
+
# waveform before t=0. Remove only the common launch-plane delay;
|
|
931
|
+
# transverse curvature and Gouy phase differences remain intact.
|
|
932
|
+
delay_patch = delay_patch - beam_reference_delay
|
|
933
|
+
scale = spec["sign"] * component_scale * spatial_amplitude * coeff_patch
|
|
934
|
+
return build_term_from_profile(
|
|
935
|
+
solver,
|
|
936
|
+
field_name=spec["field_name"],
|
|
937
|
+
offsets=spec["offsets"],
|
|
938
|
+
scale=scale,
|
|
939
|
+
delay_patch=delay_patch,
|
|
940
|
+
activation_delay_patch=None,
|
|
941
|
+
source_time=source_time,
|
|
942
|
+
omega=source_omega,
|
|
943
|
+
source_index=source_index,
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
solver._electric_source_terms.extend(build_terms_from_specs(
|
|
947
|
+
solver,
|
|
948
|
+
electric_specs,
|
|
949
|
+
magnetic_vector,
|
|
950
|
+
E_CURL_ATTR,
|
|
951
|
+
build_profile_term,
|
|
952
|
+
))
|
|
953
|
+
solver._magnetic_source_terms.extend(build_terms_from_specs(
|
|
954
|
+
solver,
|
|
955
|
+
magnetic_specs,
|
|
956
|
+
electric_vector,
|
|
957
|
+
H_CURL_ATTR,
|
|
958
|
+
build_profile_term,
|
|
959
|
+
))
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
def _prepare_mode_surface_source(solver, source, *, source_index):
|
|
963
|
+
mode_data = solve_mode_source_profile(solver, source)
|
|
964
|
+
source["effective_index"] = mode_data["effective_index"]
|
|
965
|
+
source["beta"] = mode_data["beta"]
|
|
966
|
+
source["effective_index_complex"] = mode_data.get("effective_index_complex")
|
|
967
|
+
source["beta_complex"] = mode_data.get("beta_complex")
|
|
968
|
+
source["mode_solver_kind"] = mode_data.get("mode_solver_kind")
|
|
969
|
+
|
|
970
|
+
face_side = "low" if int(source["direction_sign"]) > 0 else "high"
|
|
971
|
+
electric_specs, magnetic_specs = build_discrete_tfsf_specs(
|
|
972
|
+
mode_data["box_lower"],
|
|
973
|
+
mode_data["box_upper"],
|
|
974
|
+
)
|
|
975
|
+
electric_specs = electric_specs[_FACE_SPEC_RANGES[(source["normal_axis"], face_side)]]
|
|
976
|
+
magnetic_specs = magnetic_specs[_FACE_SPEC_RANGES[(source["normal_axis"], face_side)]]
|
|
977
|
+
source_time = source["source_time"]
|
|
978
|
+
source_omega = 2.0 * np.pi * float(source_time["frequency"])
|
|
979
|
+
normal_axis_index = _AXIS_TO_INDEX[source["normal_axis"]]
|
|
980
|
+
plane_coordinate = solver._plane_coordinate(source["normal_axis"], mode_data["plane_index"])
|
|
981
|
+
direction_sign = int(source["direction_sign"])
|
|
982
|
+
phase_speed = source_omega / max(abs(float(mode_data["beta"])), 1.0e-30)
|
|
983
|
+
|
|
984
|
+
def build_profile_term(spec, coeff_patch):
|
|
985
|
+
positions = _surface_plane_spec_positions(solver, spec)
|
|
986
|
+
component_profile = sample_mode_source_component(mode_data, positions, spec["incident_name"])
|
|
987
|
+
if spec["incident_name"].startswith("H"):
|
|
988
|
+
# TFSF face specifications store the magnetic incident component
|
|
989
|
+
# with the update-equation ``-k x E`` sign. Mode profiles expose
|
|
990
|
+
# the physical ``H = k x E / eta`` field, so convert conventions.
|
|
991
|
+
component_profile = -component_profile
|
|
992
|
+
if torch.max(torch.abs(component_profile)).item() <= 1e-12:
|
|
993
|
+
return None
|
|
994
|
+
delay_patch = (
|
|
995
|
+
float(direction_sign)
|
|
996
|
+
* (positions[..., normal_axis_index] - float(plane_coordinate))
|
|
997
|
+
/ float(phase_speed)
|
|
998
|
+
)
|
|
999
|
+
scale = spec["sign"] * component_profile * coeff_patch
|
|
1000
|
+
return build_term_from_profile(
|
|
1001
|
+
solver,
|
|
1002
|
+
field_name=spec["field_name"],
|
|
1003
|
+
offsets=spec["offsets"],
|
|
1004
|
+
scale=scale,
|
|
1005
|
+
delay_patch=delay_patch,
|
|
1006
|
+
activation_delay_patch=None,
|
|
1007
|
+
source_time=source_time,
|
|
1008
|
+
omega=source_omega,
|
|
1009
|
+
source_index=source_index,
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
for spec in electric_specs:
|
|
1013
|
+
coeff_patch = slice_coeff_patch(solver, spec, E_CURL_ATTR)
|
|
1014
|
+
term = build_profile_term(spec, coeff_patch)
|
|
1015
|
+
if term is not None:
|
|
1016
|
+
solver._electric_source_terms.append(term)
|
|
1017
|
+
for spec in magnetic_specs:
|
|
1018
|
+
coeff_patch = slice_coeff_patch(solver, spec, H_CURL_ATTR)
|
|
1019
|
+
term = build_profile_term(spec, coeff_patch)
|
|
1020
|
+
if term is not None:
|
|
1021
|
+
solver._magnetic_source_terms.append(term)
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def inject_magnetic_surface_source_terms(solver, *, time_value):
|
|
1025
|
+
if not getattr(solver, "_magnetic_source_terms", None):
|
|
1026
|
+
return
|
|
1027
|
+
apply_generic_source_terms(
|
|
1028
|
+
solver,
|
|
1029
|
+
solver._magnetic_source_terms,
|
|
1030
|
+
source_time=solver._source_time,
|
|
1031
|
+
omega=solver.source_omega,
|
|
1032
|
+
time_value=time_value,
|
|
1033
|
+
clamp_pec=False,
|
|
1034
|
+
)
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def inject_electric_surface_source_terms(solver, *, time_value):
|
|
1038
|
+
if not getattr(solver, "_electric_source_terms", None):
|
|
1039
|
+
return
|
|
1040
|
+
apply_generic_source_terms(
|
|
1041
|
+
solver,
|
|
1042
|
+
solver._electric_source_terms,
|
|
1043
|
+
source_time=solver._source_time,
|
|
1044
|
+
omega=solver.source_omega,
|
|
1045
|
+
time_value=time_value,
|
|
1046
|
+
clamp_pec=False,
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
def initialize_source_terms(solver):
|
|
1051
|
+
solver._source_terms = []
|
|
1052
|
+
solver._magnetic_source_terms = []
|
|
1053
|
+
solver._electric_source_terms = []
|
|
1054
|
+
compiled_sources = tuple(getattr(solver, "_compiled_sources", ()) or ())
|
|
1055
|
+
if not compiled_sources and getattr(solver, "_compiled_source", None) is not None:
|
|
1056
|
+
compiled_sources = (solver._compiled_source,)
|
|
1057
|
+
if not compiled_sources:
|
|
1058
|
+
return
|
|
1059
|
+
|
|
1060
|
+
for source_index, source in enumerate(compiled_sources):
|
|
1061
|
+
if source.get("injection", {}).get("kind") == "tfsf":
|
|
1062
|
+
continue
|
|
1063
|
+
if source["kind"] == "point_dipole":
|
|
1064
|
+
_prepare_point_dipole_source(solver, source, source_index=source_index)
|
|
1065
|
+
continue
|
|
1066
|
+
if source["kind"] == "mode_source":
|
|
1067
|
+
_prepare_mode_surface_source(solver, source, source_index=source_index)
|
|
1068
|
+
continue
|
|
1069
|
+
if source["kind"] in {"plane_wave", "gaussian_beam", "astigmatic_gaussian_beam"}:
|
|
1070
|
+
_prepare_power_normalized_surface_source(solver, source, source_index=source_index)
|
|
1071
|
+
continue
|
|
1072
|
+
if source["kind"] == "uniform_current":
|
|
1073
|
+
_prepare_uniform_current_source(solver, source, source_index=source_index)
|
|
1074
|
+
continue
|
|
1075
|
+
if source["kind"] == "custom_current":
|
|
1076
|
+
_prepare_custom_current_source(solver, source, source_index=source_index)
|
|
1077
|
+
continue
|
|
1078
|
+
if source["kind"] == "custom_field":
|
|
1079
|
+
_prepare_custom_field_source(solver, source, source_index=source_index)
|
|
1080
|
+
continue
|
|
1081
|
+
_prepare_surface_source(solver, source, source_index=source_index)
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
def inject_source_terms(solver, n=None, signal=None, time_value=None):
|
|
1085
|
+
if solver._source_terms is None:
|
|
1086
|
+
initialize_source_terms(solver)
|
|
1087
|
+
if not solver._source_terms:
|
|
1088
|
+
return
|
|
1089
|
+
|
|
1090
|
+
if time_value is None:
|
|
1091
|
+
if n is None:
|
|
1092
|
+
raise ValueError("Either n or time_value must be provided for source injection.")
|
|
1093
|
+
time_value = n * solver.dt
|
|
1094
|
+
|
|
1095
|
+
apply_compiled_source_terms(
|
|
1096
|
+
solver,
|
|
1097
|
+
solver._source_terms,
|
|
1098
|
+
source_time=solver._source_time,
|
|
1099
|
+
omega=solver.source_omega,
|
|
1100
|
+
time_value=time_value,
|
|
1101
|
+
signal=signal,
|
|
1102
|
+
)
|