photonforge 1.3.1__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.
Files changed (39) hide show
  1. photonforge/__init__.py +17 -12
  2. photonforge/_backend/default_project.py +398 -22
  3. photonforge/circuit_base.py +5 -40
  4. photonforge/extension.cp310-win_amd64.pyd +0 -0
  5. photonforge/live_viewer.py +2 -2
  6. photonforge/{analytic_models.py → models/analytic.py} +47 -23
  7. photonforge/models/circuit.py +684 -0
  8. photonforge/{data_model.py → models/data.py} +4 -4
  9. photonforge/{tidy3d_model.py → models/tidy3d.py} +772 -10
  10. photonforge/parametric.py +60 -28
  11. photonforge/plotting.py +1 -1
  12. photonforge/pretty.py +1 -1
  13. photonforge/thumbnails/electrical_absolute.svg +8 -0
  14. photonforge/thumbnails/electrical_adder.svg +9 -0
  15. photonforge/thumbnails/electrical_amplifier.svg +5 -0
  16. photonforge/thumbnails/electrical_differential.svg +6 -0
  17. photonforge/thumbnails/electrical_integral.svg +8 -0
  18. photonforge/thumbnails/electrical_multiplier.svg +9 -0
  19. photonforge/thumbnails/filter.svg +8 -0
  20. photonforge/thumbnails/optical_amplifier.svg +5 -0
  21. photonforge/thumbnails.py +10 -38
  22. photonforge/time_steppers/amplifier.py +353 -0
  23. photonforge/{analytic_time_steppers.py → time_steppers/analytic.py} +191 -2
  24. photonforge/{circuit_time_stepper.py → time_steppers/circuit.py} +6 -5
  25. photonforge/time_steppers/filter.py +400 -0
  26. photonforge/time_steppers/math.py +331 -0
  27. photonforge/{modulator_time_steppers.py → time_steppers/modulator.py} +9 -20
  28. photonforge/{s_matrix_time_stepper.py → time_steppers/s_matrix.py} +3 -3
  29. photonforge/{sink_time_steppers.py → time_steppers/sink.py} +6 -8
  30. photonforge/{source_time_steppers.py → time_steppers/source.py} +20 -18
  31. photonforge/typing.py +5 -0
  32. photonforge/utils.py +89 -15
  33. {photonforge-1.3.1.dist-info → photonforge-1.3.2.dist-info}/METADATA +2 -2
  34. {photonforge-1.3.1.dist-info → photonforge-1.3.2.dist-info}/RECORD +37 -27
  35. photonforge/circuit_model.py +0 -335
  36. photonforge/eme_model.py +0 -816
  37. {photonforge-1.3.1.dist-info → photonforge-1.3.2.dist-info}/WHEEL +0 -0
  38. {photonforge-1.3.1.dist-info → photonforge-1.3.2.dist-info}/entry_points.txt +0 -0
  39. {photonforge-1.3.1.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 . import typing as pft
22
- from .cache import _mode_solver_cache, _tidy3d_model_cache, cache_s_matrix
23
- from .extension import (
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 .parametric_utils import _filename_cleanup
44
- from .utils import C_0
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(data0: _MonitorData, data1: _MonitorData) -> numpy.ndarray:
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 - delta:
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 - delta:
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)