photonforge 1.3.0__cp310-cp310-win_amd64.whl → 1.3.2__cp310-cp310-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.
- photonforge/__init__.py +17 -12
- photonforge/_backend/default_project.py +398 -22
- photonforge/circuit_base.py +5 -40
- photonforge/extension.cp310-win_amd64.pyd +0 -0
- photonforge/live_viewer.py +2 -2
- photonforge/{analytic_models.py → models/analytic.py} +47 -23
- photonforge/models/circuit.py +684 -0
- photonforge/{data_model.py → models/data.py} +4 -4
- photonforge/{tidy3d_model.py → models/tidy3d.py} +772 -10
- photonforge/parametric.py +60 -28
- photonforge/plotting.py +1 -1
- photonforge/pretty.py +1 -1
- photonforge/thumbnails/electrical_absolute.svg +8 -0
- photonforge/thumbnails/electrical_adder.svg +9 -0
- photonforge/thumbnails/electrical_amplifier.svg +5 -0
- photonforge/thumbnails/electrical_differential.svg +6 -0
- photonforge/thumbnails/electrical_integral.svg +8 -0
- photonforge/thumbnails/electrical_multiplier.svg +9 -0
- photonforge/thumbnails/filter.svg +8 -0
- photonforge/thumbnails/optical_amplifier.svg +5 -0
- photonforge/thumbnails.py +10 -38
- photonforge/time_steppers/amplifier.py +353 -0
- photonforge/{analytic_time_steppers.py → time_steppers/analytic.py} +191 -2
- photonforge/{circuit_time_stepper.py → time_steppers/circuit.py} +6 -5
- photonforge/time_steppers/filter.py +400 -0
- photonforge/time_steppers/math.py +331 -0
- photonforge/{modulator_time_steppers.py → time_steppers/modulator.py} +9 -20
- photonforge/{s_matrix_time_stepper.py → time_steppers/s_matrix.py} +3 -3
- photonforge/{sink_time_steppers.py → time_steppers/sink.py} +6 -8
- photonforge/{source_time_steppers.py → time_steppers/source.py} +20 -18
- photonforge/typing.py +5 -0
- photonforge/utils.py +89 -15
- {photonforge-1.3.0.dist-info → photonforge-1.3.2.dist-info}/METADATA +2 -2
- {photonforge-1.3.0.dist-info → photonforge-1.3.2.dist-info}/RECORD +37 -27
- photonforge/circuit_model.py +0 -335
- photonforge/eme_model.py +0 -816
- {photonforge-1.3.0.dist-info → photonforge-1.3.2.dist-info}/WHEEL +0 -0
- {photonforge-1.3.0.dist-info → photonforge-1.3.2.dist-info}/entry_points.txt +0 -0
- {photonforge-1.3.0.dist-info → photonforge-1.3.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -18,9 +18,9 @@ import tidy3d
|
|
|
18
18
|
from tidy3d.components.data.data_array import DATA_ARRAY_MAP, ScalarModeFieldDataArray
|
|
19
19
|
from tidy3d.plugins.mode import ModeSolver
|
|
20
20
|
|
|
21
|
-
from
|
|
22
|
-
from
|
|
23
|
-
from
|
|
21
|
+
from .. import typing as pft
|
|
22
|
+
from ..cache import _mode_solver_cache, _tidy3d_model_cache, cache_s_matrix
|
|
23
|
+
from ..extension import (
|
|
24
24
|
Z_MAX,
|
|
25
25
|
Component,
|
|
26
26
|
FiberPort,
|
|
@@ -40,8 +40,8 @@ from .extension import (
|
|
|
40
40
|
register_model_class,
|
|
41
41
|
snap_to_grid,
|
|
42
42
|
)
|
|
43
|
-
from
|
|
44
|
-
from
|
|
43
|
+
from ..parametric_utils import _filename_cleanup
|
|
44
|
+
from ..utils import C_0
|
|
45
45
|
|
|
46
46
|
_Tidy3dBaseModel = tidy3d.components.base.Tidy3dBaseModel
|
|
47
47
|
_MonitorData = tidy3d.components.data.monitor_data.MonitorData
|
|
@@ -175,7 +175,9 @@ def _updated_tidy3d(obj: Any, path: Sequence[str], value: Any) -> _Tidy3dBaseMod
|
|
|
175
175
|
return obj.copy(update={attr: _updated_tidy3d(getattr(obj, attr), path, value)})
|
|
176
176
|
|
|
177
177
|
|
|
178
|
-
def _align_and_overlap(
|
|
178
|
+
def _align_and_overlap(
|
|
179
|
+
data0: _MonitorData, data1: _MonitorData, magnitude_warning=True
|
|
180
|
+
) -> numpy.ndarray:
|
|
179
181
|
rotations = [(0, "+"), (1, "+"), (0, "-"), (1, "-")]
|
|
180
182
|
dir0 = getattr(data0.monitor, "direction", None)
|
|
181
183
|
if dir0 is None:
|
|
@@ -273,7 +275,7 @@ def _align_and_overlap(data0: _MonitorData, data1: _MonitorData) -> numpy.ndarra
|
|
|
273
275
|
# Modes are normalized by the mode solver, so the overlap should be only a phase difference.
|
|
274
276
|
# We normalize the result to remove numerical errors introduced by the grid interpolation.
|
|
275
277
|
overlap_mag = numpy.abs(overlap)
|
|
276
|
-
if not numpy.allclose(overlap_mag, 1.0, atol=0.1):
|
|
278
|
+
if magnitude_warning and not numpy.allclose(overlap_mag, 1.0, atol=0.1):
|
|
277
279
|
max_err = overlap_mag.flat[numpy.argmax(numpy.abs(overlap_mag - 1.0))]
|
|
278
280
|
warnings.warn(
|
|
279
281
|
f"Modal overlap calculation resulted in an unexpected magnitude ({max_err}). Consider "
|
|
@@ -1554,9 +1556,9 @@ class Tidy3DModel(Model):
|
|
|
1554
1556
|
port_extension = xmin - self.bounds[0][0] + 2 * pml_gap
|
|
1555
1557
|
if self.bounds[0][1] is not None and self.bounds[0][1] < ymin - delta:
|
|
1556
1558
|
port_extension = ymin - self.bounds[0][0] + 2 * pml_gap
|
|
1557
|
-
if self.bounds[1][0] is not None and self.bounds[1][0] > xmax
|
|
1559
|
+
if self.bounds[1][0] is not None and self.bounds[1][0] > xmax + delta:
|
|
1558
1560
|
port_extension = self.bounds[1][0] - xmax + 2 * pml_gap
|
|
1559
|
-
if self.bounds[1][1] is not None and self.bounds[1][1] > ymax
|
|
1561
|
+
if self.bounds[1][1] is not None and self.bounds[1][1] > ymax + delta:
|
|
1560
1562
|
port_extension = self.bounds[1][1] - ymax + 2 * pml_gap
|
|
1561
1563
|
|
|
1562
1564
|
used_extrusions = []
|
|
@@ -2212,11 +2214,770 @@ class Tidy3DModel(Model):
|
|
|
2212
2214
|
return cls(**obj)
|
|
2213
2215
|
|
|
2214
2216
|
|
|
2217
|
+
class _EMEModelRunner:
|
|
2218
|
+
def __init__(
|
|
2219
|
+
self,
|
|
2220
|
+
simulation: tidy3d.EMESimulation,
|
|
2221
|
+
ports: dict[str, Port | FiberPort],
|
|
2222
|
+
port_groups: tuple[tuple[str], tuple[str]],
|
|
2223
|
+
mesh_refinement: tidy3d.components.grid.grid_spec.GridSpec1d | float,
|
|
2224
|
+
technology: Technology,
|
|
2225
|
+
folder_name: str,
|
|
2226
|
+
cost_estimation: bool,
|
|
2227
|
+
verbose: bool,
|
|
2228
|
+
):
|
|
2229
|
+
key = (_tidy3d_to_bytes(simulation), folder_name)
|
|
2230
|
+
runner = _tidy3d_model_cache[key]
|
|
2231
|
+
if runner is None or runner.status["message"] == "error":
|
|
2232
|
+
task_name = "EME" if "EME" not in ports else ("EME " + " ".join(ports))
|
|
2233
|
+
runner = _simulation_runner(
|
|
2234
|
+
simulation=simulation,
|
|
2235
|
+
task_name=task_name,
|
|
2236
|
+
remote_path=folder_name,
|
|
2237
|
+
cost_estimation=cost_estimation,
|
|
2238
|
+
verbose=verbose,
|
|
2239
|
+
)
|
|
2240
|
+
_tidy3d_model_cache[key] = runner
|
|
2241
|
+
self.runners = {0: runner}
|
|
2242
|
+
|
|
2243
|
+
# Port modes for decomposition
|
|
2244
|
+
filter_polarization = False
|
|
2245
|
+
for name in port_groups[0] + port_groups[1]:
|
|
2246
|
+
self.runners[name] = _ModeSolverRunner(
|
|
2247
|
+
port=ports[name],
|
|
2248
|
+
frequencies=simulation.freqs,
|
|
2249
|
+
mesh_refinement=mesh_refinement,
|
|
2250
|
+
technology=technology,
|
|
2251
|
+
center_in_origin=False,
|
|
2252
|
+
verbose=verbose,
|
|
2253
|
+
)
|
|
2254
|
+
filter_polarization = filter_polarization or (ports[name].spec.polarization != "")
|
|
2255
|
+
|
|
2256
|
+
self.ports = ports
|
|
2257
|
+
self.port_groups = port_groups
|
|
2258
|
+
self._s_matrix = None
|
|
2259
|
+
|
|
2260
|
+
# If the model uses any symmetry or polarization filter, it will impact the mode numbering
|
|
2261
|
+
# of the ports. We need to remap port modes from the symmetry-applied to the full version.
|
|
2262
|
+
self.mode_remap = simulation.symmetry != (0, 0, 0) or filter_polarization
|
|
2263
|
+
if self.mode_remap:
|
|
2264
|
+
classification = frequency_classification(simulation.freqs)
|
|
2265
|
+
use_angle_rotation = _isotropic_uniform(technology, classification)
|
|
2266
|
+
allowed = set(tidy3d.Simulation.__fields__).difference({"attrs", "type"})
|
|
2267
|
+
sim_kwargs = {k: v for k, v in simulation.dict().items() if k in allowed}
|
|
2268
|
+
full_sim = tidy3d.Simulation(run_time=1e-12, **sim_kwargs)
|
|
2269
|
+
for name in port_groups[0] + port_groups[1]:
|
|
2270
|
+
monitor = ports[name].to_tidy3d_monitor(
|
|
2271
|
+
simulation.freqs, name="M", use_angle_rotation=use_angle_rotation
|
|
2272
|
+
)
|
|
2273
|
+
mode_solver = ModeSolver(
|
|
2274
|
+
simulation=full_sim,
|
|
2275
|
+
plane=monitor.bounding_box,
|
|
2276
|
+
mode_spec=monitor.mode_spec.copy(update={"sort_spec": tidy3d.ModeSortSpec()}),
|
|
2277
|
+
freqs=simulation.freqs,
|
|
2278
|
+
direction=monitor.store_fields_direction,
|
|
2279
|
+
)
|
|
2280
|
+
self.runners[(name, "full")] = _simulation_runner(
|
|
2281
|
+
simulation=mode_solver,
|
|
2282
|
+
task_name=name + "-no_sym",
|
|
2283
|
+
remote_path=folder_name,
|
|
2284
|
+
cost_estimation=cost_estimation,
|
|
2285
|
+
verbose=verbose,
|
|
2286
|
+
)
|
|
2287
|
+
|
|
2288
|
+
@property
|
|
2289
|
+
def status(self) -> dict[str, Any]:
|
|
2290
|
+
"""Monitor S matrix computation progress."""
|
|
2291
|
+
all_stat = [runner.status for runner in self.runners.values()]
|
|
2292
|
+
if all(s["message"] == "success" for s in all_stat):
|
|
2293
|
+
message = "success"
|
|
2294
|
+
progress = 100
|
|
2295
|
+
elif any(s["message"] == "error" for s in all_stat):
|
|
2296
|
+
message = "error"
|
|
2297
|
+
progress = 100
|
|
2298
|
+
else:
|
|
2299
|
+
message = "running"
|
|
2300
|
+
progress = sum(
|
|
2301
|
+
100 if s["message"] == "success" else s["progress"] for s in all_stat
|
|
2302
|
+
) / len(all_stat)
|
|
2303
|
+
return {"progress": progress, "message": message}
|
|
2304
|
+
|
|
2305
|
+
@property
|
|
2306
|
+
def s_matrix(self) -> SMatrix:
|
|
2307
|
+
"""Get the model S matrix."""
|
|
2308
|
+
if self._s_matrix is None:
|
|
2309
|
+
# Original S matrix in EME basis
|
|
2310
|
+
eme_data = self.runners[0].data
|
|
2311
|
+
eme_modes = eme_data.port_modes_tuple
|
|
2312
|
+
eme_modes = (eme_modes[0], eme_modes[1].time_reversed_copy)
|
|
2313
|
+
|
|
2314
|
+
num_eme_modes = (
|
|
2315
|
+
eme_data.smatrix.S11.coords["mode_index_in"].size,
|
|
2316
|
+
eme_data.smatrix.S22.coords["mode_index_in"].size,
|
|
2317
|
+
)
|
|
2318
|
+
num_freqs = len(eme_data.simulation.freqs)
|
|
2319
|
+
s = numpy.empty((num_freqs, sum(num_eme_modes), sum(num_eme_modes)), dtype=complex)
|
|
2320
|
+
s[:, : num_eme_modes[0], : num_eme_modes[0]] = (
|
|
2321
|
+
eme_data.smatrix.S11.isel(sweep_index=0, drop=True)
|
|
2322
|
+
.transpose("f", "mode_index_out", "mode_index_in")
|
|
2323
|
+
.values
|
|
2324
|
+
)
|
|
2325
|
+
s[:, : num_eme_modes[0], num_eme_modes[0] :] = (
|
|
2326
|
+
eme_data.smatrix.S12.isel(sweep_index=0, drop=True)
|
|
2327
|
+
.transpose("f", "mode_index_out", "mode_index_in")
|
|
2328
|
+
.values
|
|
2329
|
+
)
|
|
2330
|
+
s[:, num_eme_modes[0] :, : num_eme_modes[0]] = (
|
|
2331
|
+
eme_data.smatrix.S21.isel(sweep_index=0, drop=True)
|
|
2332
|
+
.transpose("f", "mode_index_out", "mode_index_in")
|
|
2333
|
+
.values
|
|
2334
|
+
)
|
|
2335
|
+
s[:, num_eme_modes[0] :, num_eme_modes[0] :] = (
|
|
2336
|
+
eme_data.smatrix.S22.isel(sweep_index=0, drop=True)
|
|
2337
|
+
.transpose("f", "mode_index_out", "mode_index_in")
|
|
2338
|
+
.values
|
|
2339
|
+
)
|
|
2340
|
+
|
|
2341
|
+
# Port mode transformation matrix
|
|
2342
|
+
# M_ij = <e_i, e_j'> / <e_i, e_i>
|
|
2343
|
+
# S' = pinv(M) × S × M
|
|
2344
|
+
port_names = self.port_groups[0] + self.port_groups[1]
|
|
2345
|
+
port_num_modes = {
|
|
2346
|
+
name: self.ports[name].num_modes + self.ports[name].added_solver_modes
|
|
2347
|
+
for name in port_names
|
|
2348
|
+
}
|
|
2349
|
+
sum_modes = sum(port_num_modes.values())
|
|
2350
|
+
m = numpy.zeros((num_freqs, sum(num_eme_modes), sum_modes), dtype=complex)
|
|
2351
|
+
mode_index = 0
|
|
2352
|
+
for i in range(2):
|
|
2353
|
+
eme_mode = eme_modes[i].interpolated_copy
|
|
2354
|
+
norms = eme_mode.dot(eme_mode, conjugate=False)
|
|
2355
|
+
norms = norms.transpose("mode_index", "f").values[: num_eme_modes[i]]
|
|
2356
|
+
for name in self.port_groups[i]:
|
|
2357
|
+
num_modes = port_num_modes[name]
|
|
2358
|
+
port_data = self.runners[name].data
|
|
2359
|
+
projection = (
|
|
2360
|
+
eme_mode.outer_dot(port_data, conjugate=False)
|
|
2361
|
+
.transpose("mode_index_1", "mode_index_0", "f")
|
|
2362
|
+
.values[:, : num_eme_modes[i], :]
|
|
2363
|
+
)
|
|
2364
|
+
m_block = projection / norms
|
|
2365
|
+
if i == 0:
|
|
2366
|
+
m[:, : num_eme_modes[0], mode_index : mode_index + num_modes] = m_block.T
|
|
2367
|
+
else:
|
|
2368
|
+
m[:, num_eme_modes[0] :, mode_index : mode_index + num_modes] = m_block.T
|
|
2369
|
+
mode_index += num_modes
|
|
2370
|
+
s = numpy.linalg.pinv(m) @ s @ m
|
|
2371
|
+
|
|
2372
|
+
elements = {}
|
|
2373
|
+
j = 0
|
|
2374
|
+
for src in port_names:
|
|
2375
|
+
for src_mode in range(port_num_modes[src]):
|
|
2376
|
+
i = 0
|
|
2377
|
+
for dst in port_names:
|
|
2378
|
+
for dst_mode in range(port_num_modes[dst]):
|
|
2379
|
+
if (
|
|
2380
|
+
src_mode < self.ports[src].num_modes
|
|
2381
|
+
and dst_mode < self.ports[dst].num_modes
|
|
2382
|
+
):
|
|
2383
|
+
elements[f"{src}@{src_mode}", f"{dst}@{dst_mode}"] = s[:, i, j]
|
|
2384
|
+
i += 1
|
|
2385
|
+
j += 1
|
|
2386
|
+
|
|
2387
|
+
# If symmetry or polarization filter was used, calculate and apply mode mapping
|
|
2388
|
+
if self.mode_remap:
|
|
2389
|
+
data_sym = {
|
|
2390
|
+
name: self.runners[name].data
|
|
2391
|
+
for name, port in self.ports.items()
|
|
2392
|
+
if not isinstance(port, GaussianPort)
|
|
2393
|
+
}
|
|
2394
|
+
data_full = {
|
|
2395
|
+
name: self.runners[(name, "full")].data
|
|
2396
|
+
for name, port in self.ports.items()
|
|
2397
|
+
if not isinstance(port, GaussianPort)
|
|
2398
|
+
}
|
|
2399
|
+
elements = _mode_remap_from_symmetry(elements, self.ports, data_sym, data_full)
|
|
2400
|
+
|
|
2401
|
+
self._s_matrix = SMatrix(eme_data.simulation.freqs, elements, self.ports)
|
|
2402
|
+
|
|
2403
|
+
return self._s_matrix
|
|
2404
|
+
|
|
2405
|
+
|
|
2406
|
+
class EMEModel(Model):
|
|
2407
|
+
"""S matrix model based on Eigenmode Expansion calculation.
|
|
2408
|
+
|
|
2409
|
+
Args:
|
|
2410
|
+
eme_grid_spec: 1D grid in the that specifies the EME cells where
|
|
2411
|
+
mode solving is performed along the propagation direction.
|
|
2412
|
+
medium: Background medium. If ``None``, the technology default is
|
|
2413
|
+
used.
|
|
2414
|
+
symmetry: Component symmetries.
|
|
2415
|
+
monitors: Extra field monitors added to the simulation.
|
|
2416
|
+
structures: Additional structures included in the simulations.
|
|
2417
|
+
grid_spec: Simulation grid specification. A single float can be used
|
|
2418
|
+
to specify the ``min_steps_per_wvl`` for an auto grid.
|
|
2419
|
+
subpixel: Flag controlling subpixel averaging in the simulation
|
|
2420
|
+
grid or an instance of ``tidy3d.SubpixelSpec``.
|
|
2421
|
+
bounds: Bound overrides for the final simulation.
|
|
2422
|
+
constraint: Constraint for EME propagation. Possible values are
|
|
2423
|
+
``"passive"`` and ``"unitary"``.
|
|
2424
|
+
simulation_updates: Dictionary of updates applied to the simulation
|
|
2425
|
+
generated by this model. See example in :class:`Tidy3DModel`.
|
|
2426
|
+
verbose: Control solver verbosity.
|
|
2427
|
+
|
|
2428
|
+
If not set, the default values for the component simulations are defined
|
|
2429
|
+
based on the wavelengths used in the ``s_matrix`` call.
|
|
2430
|
+
"""
|
|
2431
|
+
|
|
2432
|
+
def __init__(
|
|
2433
|
+
self,
|
|
2434
|
+
eme_grid_spec: pft.annotate(
|
|
2435
|
+
tidy3d.components.eme.grid.EMESubgridType, brand="Tidy3dEMEGridSpec"
|
|
2436
|
+
),
|
|
2437
|
+
medium: pft.Medium | None = None,
|
|
2438
|
+
symmetry: _SymmetryType = (0, 0, 0),
|
|
2439
|
+
monitors: Sequence[_MonitorType] = (),
|
|
2440
|
+
structures: Sequence[tidy3d.Structure] = (),
|
|
2441
|
+
grid_spec: pft.PositiveFloat | tidy3d.GridSpec | None = None,
|
|
2442
|
+
subpixel: _SubpixelType = True,
|
|
2443
|
+
bounds: _BoundsType = ((None, None, None), (None, None, None)),
|
|
2444
|
+
constraint: Literal["passive", "unitary"] | None = "passive",
|
|
2445
|
+
simulation_updates: dict[str, Any] = {},
|
|
2446
|
+
verbose: bool = True,
|
|
2447
|
+
):
|
|
2448
|
+
super().__init__(
|
|
2449
|
+
eme_grid_spec=eme_grid_spec,
|
|
2450
|
+
medium=medium,
|
|
2451
|
+
symmetry=symmetry,
|
|
2452
|
+
monitors=monitors,
|
|
2453
|
+
structures=structures,
|
|
2454
|
+
grid_spec=grid_spec,
|
|
2455
|
+
subpixel=subpixel,
|
|
2456
|
+
bounds=bounds,
|
|
2457
|
+
constraint=constraint,
|
|
2458
|
+
simulation_updates=simulation_updates,
|
|
2459
|
+
verbose=verbose,
|
|
2460
|
+
)
|
|
2461
|
+
self.eme_grid_spec = eme_grid_spec
|
|
2462
|
+
self.medium = medium
|
|
2463
|
+
self.symmetry = symmetry
|
|
2464
|
+
self.monitors = monitors
|
|
2465
|
+
self.structures = structures
|
|
2466
|
+
self.grid_spec = grid_spec
|
|
2467
|
+
self.subpixel = subpixel
|
|
2468
|
+
self.bounds = bounds
|
|
2469
|
+
self.constraint = constraint
|
|
2470
|
+
self.simulation_updates = simulation_updates
|
|
2471
|
+
self.verbose = verbose
|
|
2472
|
+
|
|
2473
|
+
@staticmethod
|
|
2474
|
+
def _group_ports(ports: dict[str, Port]) -> tuple[int, tuple[tuple[str], tuple[str]]]:
|
|
2475
|
+
port_groups = {}
|
|
2476
|
+
for name, port in ports.items():
|
|
2477
|
+
if isinstance(port, Port):
|
|
2478
|
+
fraction = port.input_direction % 90
|
|
2479
|
+
if fraction > 1e-12 and 90 - fraction > 1e-12:
|
|
2480
|
+
raise RuntimeError(
|
|
2481
|
+
f"Input direction of port '{name}' is not a multiple of 90°."
|
|
2482
|
+
)
|
|
2483
|
+
direction = round(port.input_direction % 360) // 90
|
|
2484
|
+
coordinate = port.center[direction % 2]
|
|
2485
|
+
key = (coordinate, direction)
|
|
2486
|
+
elif isinstance(port, FiberPort):
|
|
2487
|
+
center, size, direction, *_ = port._axis_aligned_properties()
|
|
2488
|
+
axis = size.tolist().index(0)
|
|
2489
|
+
if axis > 1:
|
|
2490
|
+
raise RuntimeError(f"Input direction of port '{name}' is not in the xy plane.")
|
|
2491
|
+
key = (center[axis], axis + (2 if direction == "-" else 0))
|
|
2492
|
+
else:
|
|
2493
|
+
warnings.warn(
|
|
2494
|
+
f"EMEModel only works with Port and FiberPort instances. Port named '{name}' "
|
|
2495
|
+
f"of type {type(port)} will be ignored.",
|
|
2496
|
+
RuntimeWarning,
|
|
2497
|
+
2,
|
|
2498
|
+
)
|
|
2499
|
+
continue
|
|
2500
|
+
port_groups[key] = (*port_groups.get(key, ()), name)
|
|
2501
|
+
|
|
2502
|
+
if len(port_groups) == 1:
|
|
2503
|
+
key, group = next(iter(port_groups.items()))
|
|
2504
|
+
if key < 2:
|
|
2505
|
+
return (key[1], (group, ()))
|
|
2506
|
+
else:
|
|
2507
|
+
return (key[1] - 2, ((), group))
|
|
2508
|
+
|
|
2509
|
+
if len(port_groups) == 2:
|
|
2510
|
+
key0, key1 = sorted(port_groups)
|
|
2511
|
+
if key1[1] - key0[1] == 2 and key0[0] < key1[0]:
|
|
2512
|
+
return (key0[1], (port_groups[key0], port_groups[key1]))
|
|
2513
|
+
|
|
2514
|
+
raise RuntimeError(
|
|
2515
|
+
"Component ports need to be placed at 2 opposite sides, facing each other. Multiple "
|
|
2516
|
+
"ports on each side are allowed as long as they are aligned in the normal direction."
|
|
2517
|
+
)
|
|
2518
|
+
|
|
2519
|
+
def get_simulation(
|
|
2520
|
+
self, component: Component, frequencies: Sequence[float]
|
|
2521
|
+
) -> tuple[tidy3d.EMESimulation, tuple[tuple[str], tuple[str]]]:
|
|
2522
|
+
"""Create an EME simulation for a component.
|
|
2523
|
+
|
|
2524
|
+
Args:
|
|
2525
|
+
component: Instance of Component for calculation.
|
|
2526
|
+
frequencies: Sequence of frequencies for the simulation.
|
|
2527
|
+
|
|
2528
|
+
Returns:
|
|
2529
|
+
EME simulation and 2 tuples with the names of the ports on each side of the domain.
|
|
2530
|
+
"""
|
|
2531
|
+
frequencies = numpy.array(frequencies, dtype=float, ndmin=1)
|
|
2532
|
+
fmin = frequencies.min()
|
|
2533
|
+
fmax = frequencies.max()
|
|
2534
|
+
fmed = 0.5 * (fmin + fmax)
|
|
2535
|
+
max_wavelength = C_0 / fmin
|
|
2536
|
+
min_wavelength = C_0 / fmax
|
|
2537
|
+
|
|
2538
|
+
classification = frequency_classification(frequencies)
|
|
2539
|
+
medium = (
|
|
2540
|
+
component.technology.get_background_medium(classification)
|
|
2541
|
+
if self.medium is None
|
|
2542
|
+
else self.medium
|
|
2543
|
+
)
|
|
2544
|
+
# NOTE: Workaround for Simulation not accepting MultiPhysicsMedium
|
|
2545
|
+
# TODO: Remove this once support is there.
|
|
2546
|
+
if isinstance(medium, tidy3d.MultiPhysicsMedium):
|
|
2547
|
+
medium = medium.optical
|
|
2548
|
+
use_angle_rotation = _isotropic_uniform(component.technology, classification)
|
|
2549
|
+
|
|
2550
|
+
layer_refinement = _layer_steps_from_refinement(config.default_mesh_refinement)
|
|
2551
|
+
if isinstance(self.grid_spec, tidy3d.GridSpec):
|
|
2552
|
+
grid_spec = self.grid_spec
|
|
2553
|
+
mesh_refinement = config.default_mesh_refinement
|
|
2554
|
+
else:
|
|
2555
|
+
mesh_refinement = (
|
|
2556
|
+
config.default_mesh_refinement if self.grid_spec is None else self.grid_spec
|
|
2557
|
+
)
|
|
2558
|
+
layer_refinement = _layer_steps_from_refinement(mesh_refinement)
|
|
2559
|
+
grid_spec = tidy3d.GridSpec.auto(
|
|
2560
|
+
wavelength=min_wavelength,
|
|
2561
|
+
min_steps_per_wvl=mesh_refinement,
|
|
2562
|
+
min_steps_per_sim_size=mesh_refinement,
|
|
2563
|
+
)
|
|
2564
|
+
|
|
2565
|
+
extrusion_tolerance = 0
|
|
2566
|
+
if isinstance(grid_spec.grid_z, tidy3d.AutoGrid):
|
|
2567
|
+
extrusion_specs = component.technology.extrusion_specs
|
|
2568
|
+
if classification == "optical":
|
|
2569
|
+
grid_lda = min_wavelength if grid_spec.wavelength is None else grid_spec.wavelength
|
|
2570
|
+
temp_structures = [
|
|
2571
|
+
tidy3d.Structure(
|
|
2572
|
+
geometry=tidy3d.Box(size=(1, 1, 1)), medium=spec.get_medium(classification)
|
|
2573
|
+
)
|
|
2574
|
+
for spec in extrusion_specs
|
|
2575
|
+
]
|
|
2576
|
+
temp_scene = tidy3d.Scene(medium=medium, structures=temp_structures)
|
|
2577
|
+
_, eps_max = temp_scene.eps_bounds(fmed)
|
|
2578
|
+
extrusion_tolerance = grid_lda / (grid_spec.grid_z.min_steps_per_wvl * eps_max**0.5)
|
|
2579
|
+
elif len(extrusion_specs) > 0:
|
|
2580
|
+
for spec in component.technology.extrusion_specs:
|
|
2581
|
+
t = spec.limits[1] - spec.limits[0]
|
|
2582
|
+
if t > 0 and (extrusion_tolerance == 0 or extrusion_tolerance > t):
|
|
2583
|
+
extrusion_tolerance = t
|
|
2584
|
+
extrusion_tolerance = max(config.tolerance, extrusion_tolerance / layer_refinement)
|
|
2585
|
+
elif isinstance(grid_spec.grid_z, tidy3d.components.grid.grid_spec.AbstractAutoGrid):
|
|
2586
|
+
extrusion_tolerance = grid_spec.grid_z._dl_min
|
|
2587
|
+
elif isinstance(grid_spec.grid_z, tidy3d.UniformGrid):
|
|
2588
|
+
extrusion_tolerance = grid_spec.grid_z.dl
|
|
2589
|
+
elif isinstance(grid_spec.grid_z, tidy3d.CustomGrid) and len(grid_spec.grid_z.dl) > 0:
|
|
2590
|
+
extrusion_tolerance = min(grid_spec.grid_z.dl)
|
|
2591
|
+
|
|
2592
|
+
(xmin, ymin), (xmax, ymax) = component.bounds()
|
|
2593
|
+
max_bounds = max(xmax - xmin, ymax - ymin)
|
|
2594
|
+
|
|
2595
|
+
component_ports = component.select_ports(classification)
|
|
2596
|
+
|
|
2597
|
+
if classification == "optical":
|
|
2598
|
+
safe_margin = 0.3 * max_wavelength
|
|
2599
|
+
port_extension = 2 * safe_margin + max_bounds
|
|
2600
|
+
else:
|
|
2601
|
+
source_gap = max_wavelength / 100
|
|
2602
|
+
grid_scale = max_bounds / mesh_refinement
|
|
2603
|
+
safe_margin = max(max_wavelength / 100, 3 * grid_scale) + source_gap
|
|
2604
|
+
port_extension = safe_margin + 200 * grid_scale
|
|
2605
|
+
|
|
2606
|
+
if any(isinstance(p, Port) and p.bend_radius != 0 for p in component_ports.values()):
|
|
2607
|
+
warnings.warn(
|
|
2608
|
+
"Electrical ports with non-zero bending radius can result in inaccurate mode "
|
|
2609
|
+
"normalization, leading to invalid S matrices.",
|
|
2610
|
+
stacklevel=2,
|
|
2611
|
+
)
|
|
2612
|
+
|
|
2613
|
+
for port in component_ports.values():
|
|
2614
|
+
_, size, *_ = (
|
|
2615
|
+
port._axis_aligned_properties()
|
|
2616
|
+
if isinstance(port, (Port, FiberPort))
|
|
2617
|
+
else port._axis_aligned_properties(frequencies, 1.0)
|
|
2618
|
+
)
|
|
2619
|
+
port_extension = max(port_extension, size[0], size[1])
|
|
2620
|
+
|
|
2621
|
+
# Bounds override
|
|
2622
|
+
delta = port_extension - 2 * safe_margin
|
|
2623
|
+
if self.bounds[0][0] is not None and self.bounds[0][0] < xmin - delta:
|
|
2624
|
+
port_extension = xmin - self.bounds[0][0] + 2 * safe_margin
|
|
2625
|
+
if self.bounds[0][1] is not None and self.bounds[0][1] < ymin - delta:
|
|
2626
|
+
port_extension = ymin - self.bounds[0][0] + 2 * safe_margin
|
|
2627
|
+
if self.bounds[1][0] is not None and self.bounds[1][0] > xmax - delta:
|
|
2628
|
+
port_extension = self.bounds[1][0] - xmax + 2 * safe_margin
|
|
2629
|
+
if self.bounds[1][1] is not None and self.bounds[1][1] > ymax - delta:
|
|
2630
|
+
port_extension = self.bounds[1][1] - ymax + 2 * safe_margin
|
|
2631
|
+
|
|
2632
|
+
used_extrusions = []
|
|
2633
|
+
structures = [
|
|
2634
|
+
s.to_tidy3d()
|
|
2635
|
+
for s in component.extrude(
|
|
2636
|
+
port_extension,
|
|
2637
|
+
extrusion_tolerance=extrusion_tolerance,
|
|
2638
|
+
classification=classification,
|
|
2639
|
+
used_extrusions=used_extrusions,
|
|
2640
|
+
)
|
|
2641
|
+
]
|
|
2642
|
+
|
|
2643
|
+
# Sort to improve caching, but don't reorder different media
|
|
2644
|
+
i = 0
|
|
2645
|
+
while i < len(structures):
|
|
2646
|
+
current_medium = structures[i].medium
|
|
2647
|
+
j = i + 1
|
|
2648
|
+
while j < len(structures) and structures[j].medium == current_medium:
|
|
2649
|
+
j += 1
|
|
2650
|
+
# Even if j == i + 1 we want to sort internal geometries
|
|
2651
|
+
structures[i:j] = (
|
|
2652
|
+
tidy3d.Structure(geometry=geometry, medium=current_medium)
|
|
2653
|
+
for geometry in sorted(
|
|
2654
|
+
[_inner_geometry_sort(s.geometry) for s in structures[i:j]], key=_geometry_key
|
|
2655
|
+
)
|
|
2656
|
+
)
|
|
2657
|
+
i = j
|
|
2658
|
+
|
|
2659
|
+
port_structures = [
|
|
2660
|
+
structure
|
|
2661
|
+
for _, port in sorted(component_ports.items())
|
|
2662
|
+
if isinstance(port, FiberPort)
|
|
2663
|
+
for structure in port.to_tidy3d_structures()
|
|
2664
|
+
]
|
|
2665
|
+
all_structures = structures + port_structures + list(self.structures)
|
|
2666
|
+
axis, port_groups = self._group_ports(component_ports)
|
|
2667
|
+
|
|
2668
|
+
grid_snapping_points = []
|
|
2669
|
+
for name, port in component_ports.items():
|
|
2670
|
+
if isinstance(port, (Port, FiberPort)):
|
|
2671
|
+
monitor = port.to_tidy3d_monitor(
|
|
2672
|
+
frequencies, name=name, use_angle_rotation=use_angle_rotation
|
|
2673
|
+
)
|
|
2674
|
+
else:
|
|
2675
|
+
epsilon_r = _get_epsilon(port.center, all_structures, medium, frequencies)
|
|
2676
|
+
monitor = port.to_tidy3d_monitor(frequencies, medium=epsilon_r, name=name)
|
|
2677
|
+
|
|
2678
|
+
i = monitor.size.index(0)
|
|
2679
|
+
p = [None, None, None]
|
|
2680
|
+
p[i] = monitor.center[i]
|
|
2681
|
+
grid_snapping_points.append(tuple(p))
|
|
2682
|
+
|
|
2683
|
+
# Add layer refinements based on extruded layers and ports
|
|
2684
|
+
if classification == "electrical" and not isinstance(self.grid_spec, tidy3d.GridSpec):
|
|
2685
|
+
layer_refinement_specs = [
|
|
2686
|
+
_make_layer_refinement_spec(spec, layer_refinement, classification)
|
|
2687
|
+
for spec in sorted(
|
|
2688
|
+
used_extrusions, key=lambda e: e.limits[1] - e.limits[0], reverse=True
|
|
2689
|
+
)
|
|
2690
|
+
]
|
|
2691
|
+
grid_1d_update = {"max_scale": _auto_scale_from_refinement(mesh_refinement)}
|
|
2692
|
+
grid_spec = grid_spec.copy(
|
|
2693
|
+
update={
|
|
2694
|
+
"snapping_points": grid_snapping_points,
|
|
2695
|
+
"layer_refinement_specs": layer_refinement_specs,
|
|
2696
|
+
"grid_x": grid_spec.grid_x.copy(update=grid_1d_update),
|
|
2697
|
+
"grid_y": grid_spec.grid_y.copy(update=grid_1d_update),
|
|
2698
|
+
"grid_z": grid_spec.grid_z.copy(update=grid_1d_update),
|
|
2699
|
+
}
|
|
2700
|
+
)
|
|
2701
|
+
|
|
2702
|
+
# Simulation bounds
|
|
2703
|
+
zmin = 1e30
|
|
2704
|
+
zmax = -1e30
|
|
2705
|
+
for name in port_groups[0] + port_groups[1]:
|
|
2706
|
+
monitor = component_ports[name].to_tidy3d_monitor(
|
|
2707
|
+
frequencies, name="M", use_angle_rotation=use_angle_rotation
|
|
2708
|
+
)
|
|
2709
|
+
xmin = min(xmin, monitor.bounds[0][0])
|
|
2710
|
+
ymin = min(ymin, monitor.bounds[0][1])
|
|
2711
|
+
zmin = min(zmin, monitor.bounds[0][2])
|
|
2712
|
+
xmax = max(xmax, monitor.bounds[1][0])
|
|
2713
|
+
ymax = max(ymax, monitor.bounds[1][1])
|
|
2714
|
+
zmax = max(zmax, monitor.bounds[1][2])
|
|
2715
|
+
for s in structures:
|
|
2716
|
+
for i in range(2):
|
|
2717
|
+
lim = s.geometry.bounds[i][2]
|
|
2718
|
+
if -Z_MAX <= lim <= Z_MAX:
|
|
2719
|
+
zmin = min(zmin, lim)
|
|
2720
|
+
zmax = max(zmax, lim)
|
|
2721
|
+
if zmin > zmax:
|
|
2722
|
+
raise RuntimeError("No valid extrusion elements present in the component.")
|
|
2723
|
+
|
|
2724
|
+
bounds = numpy.array(((xmin, ymin, zmin), (xmax, ymax, zmax)))
|
|
2725
|
+
|
|
2726
|
+
# Bounds override
|
|
2727
|
+
for i in range(3):
|
|
2728
|
+
if self.bounds[0][i] is not None:
|
|
2729
|
+
bounds[0, i] = self.bounds[0][i]
|
|
2730
|
+
if self.bounds[1][i] is not None:
|
|
2731
|
+
bounds[1, i] = self.bounds[1][i]
|
|
2732
|
+
|
|
2733
|
+
port_offsets = []
|
|
2734
|
+
for i in range(2):
|
|
2735
|
+
sign = 2 * i - 1
|
|
2736
|
+
if len(port_groups[i]) > 0:
|
|
2737
|
+
port_offset = sign * (
|
|
2738
|
+
bounds[i][axis] - component_ports[port_groups[i][0]].center[axis]
|
|
2739
|
+
)
|
|
2740
|
+
if port_offset < safe_margin:
|
|
2741
|
+
port_offset = safe_margin
|
|
2742
|
+
bounds[i][axis] = (
|
|
2743
|
+
component_ports[port_groups[i][0]].center[axis] + sign * safe_margin
|
|
2744
|
+
)
|
|
2745
|
+
else:
|
|
2746
|
+
port_offset = 0
|
|
2747
|
+
port_offsets.append(port_offset)
|
|
2748
|
+
|
|
2749
|
+
center = tuple(snap_to_grid(v) / 2 for v in bounds[0] + bounds[1])
|
|
2750
|
+
size = tuple(snap_to_grid(v) for v in bounds[1] - bounds[0])
|
|
2751
|
+
bounding_box = tidy3d.Box(center=center, size=size)
|
|
2752
|
+
|
|
2753
|
+
eme_simulation = tidy3d.EMESimulation(
|
|
2754
|
+
freqs=frequencies,
|
|
2755
|
+
center=center,
|
|
2756
|
+
size=size,
|
|
2757
|
+
medium=medium,
|
|
2758
|
+
symmetry=self.symmetry,
|
|
2759
|
+
structures=[s for s in all_structures if bounding_box.intersects(s.geometry)],
|
|
2760
|
+
monitors=list(self.monitors),
|
|
2761
|
+
grid_spec=grid_spec,
|
|
2762
|
+
eme_grid_spec=self.eme_grid_spec,
|
|
2763
|
+
subpixel=self.subpixel,
|
|
2764
|
+
axis=axis,
|
|
2765
|
+
port_offsets=port_offsets,
|
|
2766
|
+
constraint=self.constraint,
|
|
2767
|
+
)
|
|
2768
|
+
|
|
2769
|
+
for path, value in self.simulation_updates.items():
|
|
2770
|
+
eme_simulation = _updated_tidy3d(eme_simulation, path.split("/"), value)
|
|
2771
|
+
|
|
2772
|
+
return eme_simulation, port_groups
|
|
2773
|
+
|
|
2774
|
+
@cache_s_matrix
|
|
2775
|
+
def start(
|
|
2776
|
+
self,
|
|
2777
|
+
component: Component,
|
|
2778
|
+
frequencies: Sequence[float],
|
|
2779
|
+
*,
|
|
2780
|
+
verbose: bool | None = None,
|
|
2781
|
+
cost_estimation: bool = False,
|
|
2782
|
+
**kwargs: Any,
|
|
2783
|
+
) -> _EMEModelRunner:
|
|
2784
|
+
"""Start computing the S matrix response from a component.
|
|
2785
|
+
|
|
2786
|
+
Args:
|
|
2787
|
+
component: Component from which to compute the S matrix.
|
|
2788
|
+
frequencies: Sequence of frequencies at which to perform the
|
|
2789
|
+
computation.
|
|
2790
|
+
verbose: If set, overrides the model's `verbose` attribute.
|
|
2791
|
+
cost_estimation: If set, simulations are uploaded, but not
|
|
2792
|
+
executed. S matrix will *not* be computed.
|
|
2793
|
+
**kwargs: Unused.
|
|
2794
|
+
|
|
2795
|
+
Returns:
|
|
2796
|
+
Result object with attributes ``status`` and ``s_matrix``.
|
|
2797
|
+
|
|
2798
|
+
Important:
|
|
2799
|
+
When using geometry symmetry, the mode numbering in ``inputs``
|
|
2800
|
+
is relative to the solver run *with the symmetry applied*, not
|
|
2801
|
+
the mode number presented in the final S matrix.
|
|
2802
|
+
"""
|
|
2803
|
+
simulation, port_groups = self.get_simulation(component, frequencies)
|
|
2804
|
+
folder_name = _filename_cleanup(component.name)
|
|
2805
|
+
|
|
2806
|
+
classification = frequency_classification(frequencies)
|
|
2807
|
+
component_ports = {
|
|
2808
|
+
name: port.copy(True) for name, port in component.select_ports(classification).items()
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
if verbose is None:
|
|
2812
|
+
verbose = self.verbose
|
|
2813
|
+
|
|
2814
|
+
mesh_refinement = (
|
|
2815
|
+
config.default_mesh_refinement
|
|
2816
|
+
if self.grid_spec is None or isinstance(self.grid_spec, tidy3d.GridSpec)
|
|
2817
|
+
else self.grid_spec
|
|
2818
|
+
)
|
|
2819
|
+
|
|
2820
|
+
if len(folder_name) == 0:
|
|
2821
|
+
folder_name = "default"
|
|
2822
|
+
result = _EMEModelRunner(
|
|
2823
|
+
simulation=simulation,
|
|
2824
|
+
ports=component_ports,
|
|
2825
|
+
port_groups=port_groups,
|
|
2826
|
+
mesh_refinement=mesh_refinement,
|
|
2827
|
+
technology=component.technology,
|
|
2828
|
+
folder_name=folder_name,
|
|
2829
|
+
cost_estimation=cost_estimation,
|
|
2830
|
+
verbose=verbose,
|
|
2831
|
+
)
|
|
2832
|
+
|
|
2833
|
+
return result
|
|
2834
|
+
|
|
2835
|
+
def simulation_data(
|
|
2836
|
+
self,
|
|
2837
|
+
component: Component,
|
|
2838
|
+
frequencies: Sequence[float],
|
|
2839
|
+
*,
|
|
2840
|
+
verbose: bool | None = None,
|
|
2841
|
+
show_progress: bool = True,
|
|
2842
|
+
**kwargs: Any,
|
|
2843
|
+
) -> tidy3d.EMESimulationData | None:
|
|
2844
|
+
"""Return the EME simulation data for a given component.
|
|
2845
|
+
|
|
2846
|
+
Uses the same arguments as :func:`EMEModel.start`.
|
|
2847
|
+
"""
|
|
2848
|
+
if verbose is None:
|
|
2849
|
+
verbose = self.verbose
|
|
2850
|
+
|
|
2851
|
+
if show_progress:
|
|
2852
|
+
print("Starting…", end="\r", flush=True)
|
|
2853
|
+
|
|
2854
|
+
runner = self.start(component, frequencies, verbose=verbose, **kwargs).runners[0]
|
|
2855
|
+
|
|
2856
|
+
progress_chars = "-\\|/"
|
|
2857
|
+
i = 0
|
|
2858
|
+
while True:
|
|
2859
|
+
status = runner.status
|
|
2860
|
+
message = status["message"]
|
|
2861
|
+
if message == "success":
|
|
2862
|
+
if show_progress:
|
|
2863
|
+
print("Progress: 100% ", end="\n", flush=True)
|
|
2864
|
+
return runner.data
|
|
2865
|
+
elif message == "running":
|
|
2866
|
+
if show_progress:
|
|
2867
|
+
p = max(0, min(100, int(status.get("progress", 0))))
|
|
2868
|
+
c = progress_chars[i]
|
|
2869
|
+
i = (i + 1) % len(progress_chars)
|
|
2870
|
+
print(f"Progress: {p}% {c}", end="\r", flush=True)
|
|
2871
|
+
time.sleep(0.3)
|
|
2872
|
+
elif message == "error":
|
|
2873
|
+
if show_progress:
|
|
2874
|
+
print("Progress: error", end="\n", flush=True)
|
|
2875
|
+
raise RuntimeError("Batch run resulted in error.")
|
|
2876
|
+
else:
|
|
2877
|
+
raise RuntimeError(f"Status message unknown: {message:r}.")
|
|
2878
|
+
|
|
2879
|
+
def data_path_for(self, *_, **__):
|
|
2880
|
+
"""DEPRECATED
|
|
2881
|
+
|
|
2882
|
+
Use :func:`EMEModel.simulation_data`.
|
|
2883
|
+
"""
|
|
2884
|
+
raise RuntimeError(
|
|
2885
|
+
"This function has been deprecated. Please use 'EMEModel.simulation_data'."
|
|
2886
|
+
)
|
|
2887
|
+
|
|
2888
|
+
def data_file_for(self, *_, **__):
|
|
2889
|
+
"""DEPRECATED
|
|
2890
|
+
|
|
2891
|
+
Use :func:`EMEModel.simulation_data`.
|
|
2892
|
+
"""
|
|
2893
|
+
raise RuntimeError(
|
|
2894
|
+
"This function has been deprecated. Please use 'EMEModel.simulation_data'."
|
|
2895
|
+
)
|
|
2896
|
+
|
|
2897
|
+
def simulation_data_for(self, *_, **__):
|
|
2898
|
+
"""DEPRECATED
|
|
2899
|
+
|
|
2900
|
+
Use :func:`EMEModel.simulation_data`.
|
|
2901
|
+
"""
|
|
2902
|
+
raise RuntimeError(
|
|
2903
|
+
"This function has been deprecated. Please use 'EMEModel.simulation_data'."
|
|
2904
|
+
)
|
|
2905
|
+
|
|
2906
|
+
# Deprecated: kept for backwards compatibility with old phf files
|
|
2907
|
+
@classmethod
|
|
2908
|
+
def from_bytes(cls, byte_repr: bytes) -> "EMEModel":
|
|
2909
|
+
"""De-serialize this model."""
|
|
2910
|
+
version = byte_repr[0]
|
|
2911
|
+
if version == 1:
|
|
2912
|
+
obj = dict(_from_bytes(byte_repr[1:]))
|
|
2913
|
+
|
|
2914
|
+
elif version == 0:
|
|
2915
|
+
n_size = struct.calcsize("<BL")
|
|
2916
|
+
n = struct.unpack("<L", byte_repr[1:n_size])[0]
|
|
2917
|
+
cursor = struct.calcsize("<BL" + n * "Q")
|
|
2918
|
+
lengths = struct.unpack("<" + n * "Q", byte_repr[n_size:cursor])
|
|
2919
|
+
|
|
2920
|
+
obj = json.loads(byte_repr[cursor : cursor + lengths[0]].decode("utf-8"))
|
|
2921
|
+
cursor += lengths[0]
|
|
2922
|
+
|
|
2923
|
+
models = [None] * (n - 1)
|
|
2924
|
+
for i, length in enumerate(lengths[1:]):
|
|
2925
|
+
models[i] = _tidy3d_from_bytes(byte_repr[cursor : cursor + length])
|
|
2926
|
+
cursor += length
|
|
2927
|
+
|
|
2928
|
+
if cursor != len(byte_repr):
|
|
2929
|
+
raise RuntimeError("Invalid byte representation for Tidy3DModel.")
|
|
2930
|
+
|
|
2931
|
+
indices = obj.pop("_tidy3d_indices_")
|
|
2932
|
+
for name, (i, j) in indices.items():
|
|
2933
|
+
if j < 0:
|
|
2934
|
+
obj[name] = models[i]
|
|
2935
|
+
else:
|
|
2936
|
+
obj[name] = [models[m] for m in range(i, j)]
|
|
2937
|
+
|
|
2938
|
+
# zlib-compressed json used before versioning
|
|
2939
|
+
elif version == 0x78:
|
|
2940
|
+
obj = json.loads(zlib.decompress(byte_repr).decode("utf-8"))
|
|
2941
|
+
obj = _decode_arrays(obj)
|
|
2942
|
+
|
|
2943
|
+
item = obj.get("eme_grid_spec")
|
|
2944
|
+
if isinstance(item, dict):
|
|
2945
|
+
obj["eme_grid_spec"] = pydantic.v1.parse_obj_as(
|
|
2946
|
+
tidy3d.components.eme.grid.EMESubgridType, item, type_name=item["type"]
|
|
2947
|
+
)
|
|
2948
|
+
|
|
2949
|
+
item = obj.get("medium")
|
|
2950
|
+
if isinstance(item, dict):
|
|
2951
|
+
obj["medium"] = pydantic.v1.parse_obj_as(
|
|
2952
|
+
tidy3d.components.medium.MediumType3D, item, type_name=item["type"]
|
|
2953
|
+
)
|
|
2954
|
+
|
|
2955
|
+
item = obj.get("grid_spec")
|
|
2956
|
+
if isinstance(item, dict):
|
|
2957
|
+
obj["grid_spec"] = pydantic.v1.parse_obj_as(
|
|
2958
|
+
tidy3d.GridSpec, item, type_name=item["type"]
|
|
2959
|
+
)
|
|
2960
|
+
|
|
2961
|
+
obj["monitors"] = [
|
|
2962
|
+
pydantic.v1.parse_obj_as(
|
|
2963
|
+
tidy3d.components.types.monitor.MonitorType, mon, type_name=mon["type"]
|
|
2964
|
+
)
|
|
2965
|
+
for mon in obj.get("monitors", ())
|
|
2966
|
+
]
|
|
2967
|
+
|
|
2968
|
+
obj["structures"] = [
|
|
2969
|
+
pydantic.v1.parse_obj_as(tidy3d.Structure, s, type_name=s["type"])
|
|
2970
|
+
for s in obj.get("structures", ())
|
|
2971
|
+
]
|
|
2972
|
+
|
|
2973
|
+
return cls(**obj)
|
|
2974
|
+
|
|
2975
|
+
|
|
2215
2976
|
# Note: Kept for backwards compatibility. Do not use: horrible performance.
|
|
2216
2977
|
def _decode_arrays(obj: Any) -> Any:
|
|
2217
2978
|
if isinstance(obj, dict):
|
|
2218
2979
|
if len(obj) == 1:
|
|
2219
|
-
import xarray
|
|
2980
|
+
import xarray # noqa: PLC0415
|
|
2220
2981
|
|
|
2221
2982
|
k, v = next(iter(obj.items()))
|
|
2222
2983
|
if k == "PhotonForge xarray.Dataset":
|
|
@@ -2234,3 +2995,4 @@ def _decode_arrays(obj: Any) -> Any:
|
|
|
2234
2995
|
|
|
2235
2996
|
|
|
2236
2997
|
register_model_class(Tidy3DModel)
|
|
2998
|
+
register_model_class(EMEModel)
|