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
photonforge/__init__.py CHANGED
@@ -70,17 +70,17 @@ from .utils import (
70
70
  from .parametric_utils import parametric_component, parametric_technology
71
71
  from .plotting import plot_s_matrix, tidy3d_plot
72
72
  from .netlist import component_from_netlist
73
- from .tidy3d_model import (
73
+ from .models.tidy3d import (
74
74
  Tidy3DModel,
75
+ EMEModel,
75
76
  abort_pending_tasks,
76
77
  port_modes,
77
78
  _tidy3d_to_str,
78
79
  _tidy3d_to_bytes,
79
80
  _tidy3d_from_bytes,
80
81
  )
81
- from .eme_model import EMEModel
82
- from .circuit_model import CircuitModel
83
- from .analytic_models import (
82
+ from .models.circuit import CircuitModel, DirectionalCouplerCircuitModel
83
+ from .models.analytic import (
84
84
  ModelResult,
85
85
  TwoPortModel,
86
86
  PowerSplitterModel,
@@ -94,24 +94,29 @@ from .analytic_models import (
94
94
  AnalyticDirectionalCouplerModel,
95
95
  AnalyticMZIModel,
96
96
  )
97
- from .data_model import DataModel
98
- from .analytic_time_steppers import DelayedTimeStepper
99
- from .s_matrix_time_stepper import SMatrixTimeStepper
100
- from .circuit_time_stepper import CircuitTimeStepper
101
- from .source_time_steppers import (
97
+ from .models.data import DataModel
98
+ from .time_steppers.amplifier import OpticalAmplifierTimeStepper, ElectricalAmplifierTimeStepper
99
+ from .time_steppers.filter import FilterTimeStepper
100
+ from .time_steppers.analytic import DelayedTimeStepper, AnalyticWaveguideTimeStepper
101
+ from .time_steppers.s_matrix import SMatrixTimeStepper
102
+ from .time_steppers.circuit import CircuitTimeStepper
103
+ from .time_steppers.math import ExpressionTimeStepper, DifferentialTimeStepper, IntegralTimeStepper
104
+ from .time_steppers.source import (
102
105
  CWLaserTimeStepper,
103
106
  DMLaserTimeStepper,
104
107
  OpticalPulseTimeStepper,
105
108
  OpticalNoiseTimeStepper,
106
109
  WaveformTimeStepper,
107
110
  )
108
- from .sink_time_steppers import PhotodiodeTimeStepper
109
- from .modulator_time_steppers import PhaseModTimeStepper, TerminatedModTimeStepper
111
+ from .time_steppers.sink import PhotodiodeTimeStepper
112
+ from .time_steppers.modulator import PhaseModTimeStepper, TerminatedModTimeStepper
110
113
  from .pretty import _Tree, LayerTable, PortSpecTable, ExtrusionTable
111
114
  from .thumbnails import thumbnails
115
+
116
+ from . import live_viewer
117
+ from . import monte_carlo
112
118
  from . import parametric
113
119
  from . import stencil
114
- from . import monte_carlo
115
120
 
116
121
  from tidy3d.config import get_manager as _gm
117
122
 
@@ -6,13 +6,97 @@ import tidy3d as _td
6
6
 
7
7
  import photonforge as _pf
8
8
  import photonforge.typing as _pft
9
- from photonforge.analytic_models import _add_bb_text, _bb_layer
10
- from photonforge.source_time_steppers import RIN as _RIN
9
+ from photonforge.models.analytic import _add_bb_text, _bb_layer
10
+ from photonforge.time_steppers.source import RIN as _RIN
11
11
 
12
12
  _ThermoOpticCoeff = _pft.annotate(float, label="dn/dT", units="1/K")
13
13
  _LossTemperatureCoeff = _pft.annotate(float, label="dL/dT", units="dB/μm/K")
14
14
 
15
15
 
16
+ class _PassThroughModel(_pf.Model):
17
+ def __init__(
18
+ self,
19
+ *,
20
+ t: _pft.annotate(float, minimum=-1.0, maximum=1.0) = 1.0,
21
+ r: _pft.annotate(float, minimum=-1.0, maximum=1.0) = 0.0,
22
+ ):
23
+ super().__init__(t=t, r=r)
24
+
25
+ def black_box_component(
26
+ self,
27
+ num_ports: int,
28
+ port_spec: str | _pf.PortSpec | None = None,
29
+ technology: _pf.Technology | None = None,
30
+ name: str | None = None,
31
+ ) -> _pf.Component:
32
+ """Create a black-box component using this model for testing.
33
+
34
+ Args:
35
+ port_spec: Port specification used in the component. If ``None``,
36
+ look for ``"port_spec"`` in :attr:`config.default_kwargs`.
37
+ technology: Component technology. If ``None``, the default
38
+ technology is used.
39
+ name: Component name. If ``None`` a default is used.
40
+
41
+ Returns:
42
+ Component with ports and model.
43
+ """
44
+ model_name = self.__class__.__name__[:-5]
45
+ component = _pf.Component(
46
+ f"BB{model_name}" if name is None else name, technology=technology
47
+ )
48
+
49
+ width = port_spec.width
50
+ length = width * 8
51
+
52
+ profiles = port_spec.path_profiles_list()
53
+ if len(profiles) == 0:
54
+ profiles = [(width, 0, _bb_layer)]
55
+
56
+ for w, g, layer in profiles:
57
+ component.add(layer, _pf.Path((0, 0), w, g).segment((length, 0)))
58
+
59
+ _add_bb_text(component, width)
60
+
61
+ if isinstance(num_ports, tuple):
62
+ num_by_size = {0: [0, 0], 90: [0, 0], -90: [0, 0], 180: [0, 0]}
63
+ for a in num_ports:
64
+ num_by_size[a][1] += 1
65
+ for a in num_ports:
66
+ i, n = num_by_size[a]
67
+ num_by_size[a][0] += 1
68
+ if a == 0 or a == 180:
69
+ x = 0 if a == 0 else length
70
+ y = 0 if n == 1 else (-width / 4 + width / 2 * i / (n - 1))
71
+ else:
72
+ x = (length / 2) if n == 1 else (length / 4 + length / 2 * i / (n - 1))
73
+ y = (-width / 2) if a == 90 else (width / 2)
74
+ component.add_port(_pf.Port((x, y), a, port_spec))
75
+ else:
76
+ for i in range(num_ports):
77
+ a = [0, 180, 90, -90][i % 4]
78
+ x = [0, length, length / 2, length / 2][i % 4]
79
+ y = [0, 0, -width / 2, width / 2][i % 4]
80
+ component.add_port(_pf.Port((x, y), a, port_spec))
81
+
82
+ component.add_model(self, model_name)
83
+ return component
84
+
85
+ def start(self, component, frequencies, **kwargs):
86
+ ports = component.select_ports("electrical")
87
+ port_names = sorted(ports.keys())
88
+ t = _np.full(len(frequencies), self.parametric_kwargs["t"], dtype=complex)
89
+ r = _np.full(len(frequencies), self.parametric_kwargs["r"], dtype=complex)
90
+ elements = {
91
+ **{(f"{port_names[i - 1]}@0", f"{port_names[i]}@0"): t for i in range(len(port_names))},
92
+ **{(f"{p}@0", f"{p}@0"): r for p in port_names},
93
+ }
94
+ return _pf.SMatrix(frequencies, elements, ports)
95
+
96
+
97
+ _pf.register_model_class(_PassThroughModel)
98
+
99
+
16
100
  @_pf.parametric_component
17
101
  def straight_waveguide(
18
102
  *,
@@ -563,9 +647,9 @@ def electrical_termination(
563
647
  r_mag = float(_np.clip(r_mag, 0.0, 1.0)) # safety clamp
564
648
 
565
649
  # attach the model
566
- model = _pf.TerminationModel(r=r_mag)
650
+ model = _PassThroughModel(r=r_mag)
567
651
  comp = model.black_box_component(
568
- _pf.virtual_port_spec(classification="electrical"), name="Electrical Termination"
652
+ 1, _pf.virtual_port_spec(classification="electrical"), name="Electrical Termination"
569
653
  )
570
654
  comp.properties.__thumbnail__ = "electrical_termination"
571
655
  return comp
@@ -766,7 +850,7 @@ def polarization_beam_splitter(
766
850
  r_01: complex = 0.0 + 0.0j, # reflections at P0 (mode 1)
767
851
  r_11: complex = 0.0 + 0.0j, # reflections at P1 (mode 1)
768
852
  r_21: complex = 0.0 + 0.0j, # reflections at P2 (mode 1)
769
- mode_routing: dict[int, int] = {0: 1, 1: 2}, # mode → preferred output port in {1,2}
853
+ mode_routing: _pft.annotate(_Sequence[int], minItems=2, maxItems=2) = (1, 2),
770
854
  ):
771
855
  """
772
856
  Polarization Beam Splitter (PBS), three ports, two modes, no mode mixing.
@@ -787,9 +871,8 @@ def polarization_beam_splitter(
787
871
  Reflection amplitudes at P0, P1, P2 for mode 0.
788
872
  r_01, r_11, r_21 : complex
789
873
  Reflection amplitudes at P0, P1, P2 for mode 1.
790
- mode_routing : dict[int, int]
791
- Maps mode index preferred output port index in {1,2}. Default {0:1, 1:2}.
792
- The “other” port is the remaining one in {1,2}.
874
+ mode_routing : tuple[int, int]
875
+ Maps mode index to preferred output port index in {1,2}.
793
876
 
794
877
  Notes
795
878
  -----
@@ -804,12 +887,11 @@ def polarization_beam_splitter(
804
887
  xm = [x_0, x_1]
805
888
  t1 = [0.0 + 0.0j, 0.0 + 0.0j]
806
889
  t2 = [0.0 + 0.0j, 0.0 + 0.0j]
807
- for m in (0, 1):
808
- pref = mode_routing.get(m, 1) # default to port 1 if missing
890
+ for m, pref in enumerate(mode_routing):
809
891
  if pref == 1:
810
892
  t1[m] = tm[m]
811
893
  t2[m] = xm[m]
812
- else: # pref == 2
894
+ elif pref == 2:
813
895
  t1[m] = xm[m]
814
896
  t2[m] = tm[m]
815
897
 
@@ -848,7 +930,7 @@ def polarization_splitter_grating_coupler(
848
930
  r_01: complex = 0.0 + 0.0j, # reflections at P0 (mode 1)
849
931
  r_11: complex = 0.0 + 0.0j, # reflections at P1 (mode 1)
850
932
  r_21: complex = 0.0 + 0.0j, # reflections at P2 (mode 1)
851
- mode_routing: dict[int, int] = {0: 1, 1: 2}, # mode → preferred output port in {1,2}
933
+ mode_routing: _pft.annotate(_Sequence[int], minItems=2, maxItems=2) = (1, 2),
852
934
  ):
853
935
  """
854
936
  Polarization Splitter Grating Coupler (PSGC), three ports, two modes, no mode mixing.
@@ -873,9 +955,8 @@ def polarization_splitter_grating_coupler(
873
955
  Reflection amplitudes at ports P0, P1, P2 for mode 0.
874
956
  r_01, r_11, r_21 : complex
875
957
  Reflection amplitudes at ports P0, P1, P2 for mode 1.
876
- mode_routing : dict[int, int]
877
- Maps mode index preferred output port index in {1,2}. Default {0:1, 1:2}.
878
- The “other” port is the remaining one.
958
+ mode_routing : tuple[int, int]
959
+ Maps mode index to preferred output port index in {1,2}.
879
960
 
880
961
  Notes
881
962
  -----
@@ -887,12 +968,11 @@ def polarization_splitter_grating_coupler(
887
968
  xm = [x_0, x_1]
888
969
  t1 = [0.0 + 0.0j, 0.0 + 0.0j]
889
970
  t2 = [0.0 + 0.0j, 0.0 + 0.0j]
890
- for m in (0, 1):
891
- pref = mode_routing.get(m, 1)
971
+ for m, pref in enumerate(mode_routing):
892
972
  if pref == 1:
893
973
  t1[m] = tm[m]
894
974
  t2[m] = xm[m]
895
- else:
975
+ elif pref == 2:
896
976
  t1[m] = xm[m]
897
977
  t2[m] = tm[m]
898
978
 
@@ -1345,8 +1425,8 @@ def photodiode(
1345
1425
  def signal_source(
1346
1426
  *,
1347
1427
  frequency: _pft.Frequency = 10e9,
1348
- amplitude: _pft.annotate(float, units="√W") = 1,
1349
- offset: _pft.annotate(float, units="√W") = 0,
1428
+ amplitude: _pft.FieldAmplitude = 1,
1429
+ offset: _pft.FieldAmplitude = 0,
1350
1430
  start: _pft.Time | None = None,
1351
1431
  stop: _pft.Time | None = None,
1352
1432
  skew: _pft.Fraction = 0.5,
@@ -1388,7 +1468,7 @@ def signal_source(
1388
1468
  prbs: PRBS polinomial degree. Value 0 disables PRBS.
1389
1469
  seed: Random number generator seed to ensure reproducibility.
1390
1470
  """
1391
- model = _pf.TerminationModel()
1471
+ model = _PassThroughModel()
1392
1472
  model.time_stepper = _pf.WaveformTimeStepper(
1393
1473
  frequency=frequency,
1394
1474
  amplitude=amplitude,
@@ -1407,12 +1487,308 @@ def signal_source(
1407
1487
  prbs=prbs,
1408
1488
  )
1409
1489
  comp = _pf.Reference(
1410
- model.black_box_component(_pf.virtual_port_spec(classification="electrical")), rotation=180
1490
+ model.black_box_component(1, _pf.virtual_port_spec(classification="electrical")),
1491
+ rotation=180,
1411
1492
  ).transformed_component("Source")
1412
1493
  comp.properties.__thumbnail__ = "source"
1413
1494
  return comp
1414
1495
 
1415
1496
 
1497
+ @_pf.parametric_component
1498
+ def filter(
1499
+ *,
1500
+ family: _typ.Literal[
1501
+ "digital",
1502
+ "rc",
1503
+ "butterworth",
1504
+ "bessel",
1505
+ "cheby1",
1506
+ "rectangular",
1507
+ "gaussian",
1508
+ ] = "rc",
1509
+ shape: _typ.Literal["lp", "hp", "bp", "bs"] = "lp",
1510
+ f_cutoff: _pft.Frequency
1511
+ | _pft.annotate(_Sequence[_pft.Frequency], minItems=2, maxItems=2) = 20e9,
1512
+ order: _pft.PositiveInt = 1,
1513
+ ripple: _pft.Loss = 0,
1514
+ window: str
1515
+ | tuple[str, float]
1516
+ | tuple[str, float, float]
1517
+ | tuple[str, _Sequence[float]] = "hann",
1518
+ a: _Sequence[complex] = (1.0,),
1519
+ b: _Sequence[complex] = (),
1520
+ taps: _pft.PositiveInt = 101,
1521
+ insertion_loss: _pft.Loss = 0,
1522
+ ):
1523
+ """Filter time stepper for electrical signals.
1524
+
1525
+ Args:
1526
+ family: Filter family.
1527
+ shape: Filter shape.
1528
+ f_cutoff: Cutoff frequency for low- and high-pass filter shapes. For
1529
+ ``family=="tunable_lp_rc"``, this can be an :class:`Interpolator`
1530
+ to provide the dependency with the input voltage. For band-pass
1531
+ and band-stop shapes, this is a 2-value sequence with low and high
1532
+ cutoff frequencies.
1533
+ order: Filter order.
1534
+ ripple: Maximum ripple for Chebyshev filters.
1535
+ window: Window specification for rectangular filters. Please consult
1536
+ the help for ``scipy.signal.firwin`` for valid options.
1537
+ a: Recursive (denominator) coefficients for a digital IIR filter.
1538
+ b: Direct (numerator) coefficients for a digital FIR or IIR filter.
1539
+ taps: Length of rectangular or Gaussian filters.
1540
+ insertion_loss: Insertion loss added to the filter response.
1541
+ """
1542
+ model = _PassThroughModel()
1543
+ model.time_stepper = _pf.FilterTimeStepper(
1544
+ family=family,
1545
+ shape=shape,
1546
+ f_cutoff=f_cutoff,
1547
+ order=order,
1548
+ ripple=ripple,
1549
+ window=window,
1550
+ a=a,
1551
+ b=b,
1552
+ taps=taps,
1553
+ insertion_loss=insertion_loss,
1554
+ )
1555
+ comp = model.black_box_component(
1556
+ 2, _pf.virtual_port_spec(classification="electrical"), name="Filter"
1557
+ )
1558
+ comp.properties.__thumbnail__ = "filter"
1559
+ return comp
1560
+
1561
+
1562
+ # _interp = _pf.Interpolator([0, 10], [20e9, 25e9])
1563
+ #
1564
+ #
1565
+ # @_pf.parametric_component
1566
+ # def tunable_filter(
1567
+ # *,
1568
+ # f_cutoff: _pf.Interpolator = _interp,
1569
+ # insertion_loss: _pft.Loss = 0,
1570
+ # z0: _pft.Impedance = 50.0,
1571
+ # ):
1572
+ # """Tunable first-order LP filter time stepper for electrical signals.
1573
+ #
1574
+ # Args:
1575
+ # f_cutoff: Voltage-dependent cutoff frequency.
1576
+ # insertion_loss: Insertion loss added to the filter response.
1577
+ # z0: Characteristic impedance of the electrical port used to convert
1578
+ # the input control field amplitude to voltage.
1579
+ # """
1580
+ # model = _pf.DataModel(s_array=_np.zeros((1, 3, 3)), frequencies=[1e9])
1581
+ # model.time_stepper = _pf.FilterTimeStepper(
1582
+ # family="tunable_lp_rc", f_cutoff=f_cutoff, insertion_loss=insertion_loss, z0=z0
1583
+ # )
1584
+ # comp = model.black_box_component(
1585
+ # _pf.virtual_port_spec(classification="electrical"), name="Filter"
1586
+ # )
1587
+ # comp.properties.__thumbnail__ = "filter"
1588
+ # return comp
1589
+
1590
+
1591
+ @_pf.parametric_component
1592
+ def optical_amplifier(
1593
+ *,
1594
+ gain: _pft.Gain = 15,
1595
+ noise_figure: _pft.Gain | None = None,
1596
+ seed: _pft.NonNegativeInt | None = None,
1597
+ ):
1598
+ r"""Optical amplifier with constant gain.
1599
+
1600
+ Args:
1601
+ gain: The amplifier's power gain.
1602
+ noise_figure: The amplifier's noise figure (NF).
1603
+ seed: Random number generator seed to ensure reproducibility.
1604
+ """
1605
+ model = _pf.TwoPortModel(t=1)
1606
+ model.time_stepper = _pf.OpticalAmplifierTimeStepper(
1607
+ gain=gain, noise_figure=noise_figure, seed=seed
1608
+ )
1609
+ comp = model.black_box_component(port_spec=_pf.virtual_port_spec(), name="Optical Amplifier")
1610
+ comp.properties.__thumbnail__ = "optical_amplifier"
1611
+ return comp
1612
+
1613
+
1614
+ @_pf.parametric_component
1615
+ def electrical_amplifier(
1616
+ *,
1617
+ gain: _pft.Gain = 15,
1618
+ f_3dB: _pft.annotate(_pft.Frequency | None, label="f 3dB") = None,
1619
+ saturation_power: _pft.Power_dBm | None = None,
1620
+ compression_power: _pft.Power_dBm | None = None,
1621
+ ip3: _pft.annotate(_pft.Power_dBm | None, label="IP3") = None,
1622
+ noise_figure: _pft.Gain | None = None,
1623
+ r0: _pft.annotate(float, minimum=-1, maximum=1) = 0,
1624
+ r1: _pft.annotate(float, minimum=-1, maximum=1) = 0,
1625
+ seed: _pft.NonNegativeInt | None = None,
1626
+ ):
1627
+ r"""Electrical amplifier with constant gain.
1628
+
1629
+ Args:
1630
+ gain: The amplifier's power gain.
1631
+ f_3dB: -3 dB frequency cutoff for bandwidth limiting.
1632
+ saturation_power: Output saturation power.
1633
+ compression_power: 1 dB compression power.
1634
+ ip3: Third order intercept point.
1635
+ noise_figure: The amplifier's noise figure (NF).
1636
+ r0: Reflection coefficient for the input port.
1637
+ r1: Reflection coefficient for the output port.
1638
+ seed: Random number generator seed to ensure reproducibility.
1639
+ """
1640
+ model = _PassThroughModel()
1641
+ model.time_stepper = _pf.ElectricalAmplifierTimeStepper(
1642
+ gain=gain,
1643
+ f_3dB=f_3dB,
1644
+ saturation_power=saturation_power,
1645
+ compression_power=compression_power,
1646
+ ip3=ip3,
1647
+ noise_figure=noise_figure,
1648
+ r0=r0,
1649
+ r1=r1,
1650
+ seed=seed,
1651
+ )
1652
+ comp = model.black_box_component(
1653
+ 2, port_spec=_pf.virtual_port_spec(classification="electrical"), name="Electrical Amplifier"
1654
+ )
1655
+ comp.properties.__thumbnail__ = "electrical_amplifier"
1656
+ return comp
1657
+
1658
+
1659
+ @_pf.parametric_component
1660
+ def scaler(*, scale: float = 1.0):
1661
+ r"""Electrical scaler: constant gain multiplier.
1662
+
1663
+ Args:
1664
+ scale: Output scaling factor.
1665
+ """
1666
+ model = _PassThroughModel()
1667
+ model.time_stepper = _pf.ExpressionTimeStepper(expressions={"E1": f"{scale} * E0"})
1668
+ comp = model.black_box_component(
1669
+ 2, port_spec=_pf.virtual_port_spec(classification="electrical"), name="Scaler"
1670
+ )
1671
+ comp.properties.__thumbnail__ = "electrical_amplifier"
1672
+ return comp
1673
+
1674
+
1675
+ @_pf.parametric_component
1676
+ def absolute(*, scale: float = 1.0):
1677
+ r"""Absolute value of the input signal.
1678
+
1679
+ Args:
1680
+ scale: Output scaling factor.
1681
+ """
1682
+ model = _PassThroughModel()
1683
+ model.time_stepper = _pf.ExpressionTimeStepper(expressions={"E1": f"{scale} * abs(E0)"})
1684
+ comp = model.black_box_component(
1685
+ 2, port_spec=_pf.virtual_port_spec(classification="electrical"), name="Absolute"
1686
+ )
1687
+ comp.properties.__thumbnail__ = "electrical_absolute"
1688
+ return comp
1689
+
1690
+
1691
+ @_pf.parametric_component
1692
+ def adder(*, scale: float = 1.0, weight0: float = 1.0, weight1: float = 1.0):
1693
+ r"""Electrical adder: linearly combine 2 inputs.
1694
+
1695
+ Args:
1696
+ scale: Output scaling factor.
1697
+ weight0: Weight applied to the signal from the first port.
1698
+ weight1: Weight applied to the signal from the second port.
1699
+ """
1700
+ model = _PassThroughModel()
1701
+ model.time_stepper = _pf.ExpressionTimeStepper(
1702
+ expressions={"E2": f"{scale} * ({weight0} * E0 + {weight1} * E1)"}
1703
+ )
1704
+ comp = model.black_box_component(
1705
+ (0, 0, 180), port_spec=_pf.virtual_port_spec(classification="electrical"), name="Adder"
1706
+ )
1707
+ comp.properties.__thumbnail__ = "electrical_adder"
1708
+ return comp
1709
+
1710
+
1711
+ @_pf.parametric_component
1712
+ def multiplier(*, scale: float = 1.0, exponent0: float = 1.0, exponent1: float = 1.0):
1713
+ r"""Electrical multiplier: compute the product of 2 inputs.
1714
+
1715
+ Args:
1716
+ scale: Output scaling factor.
1717
+ exponent0: Exponent used for the first input. Fractional values will
1718
+ fail on negative inputs.
1719
+ exponent1: Exponent used for the second input. Fractional values
1720
+ will fail on negative inputs.
1721
+ """
1722
+ model = _PassThroughModel()
1723
+ model.time_stepper = _pf.ExpressionTimeStepper(
1724
+ expressions={"E2": f"{scale} * (E0^{exponent0} * E1^{exponent1})"}
1725
+ )
1726
+ comp = model.black_box_component(
1727
+ (0, 0, 180), port_spec=_pf.virtual_port_spec(classification="electrical"), name="Multiplier"
1728
+ )
1729
+ comp.properties.__thumbnail__ = "electrical_multiplier"
1730
+ return comp
1731
+
1732
+
1733
+ @_pf.parametric_component
1734
+ def differentiator(
1735
+ *, scale: float = 1.0, scheme: _typ.Literal["backwards", "central"] = "backwards"
1736
+ ):
1737
+ r"""Derivative of the input signal.
1738
+
1739
+ Args:
1740
+ scheme: Differentiation scheme.
1741
+ scale: Output scaling factor.
1742
+ """
1743
+ model = _PassThroughModel()
1744
+ model.time_stepper = _pf.DifferentialTimeStepper(scheme=scheme, scale=scale)
1745
+ comp = model.black_box_component(
1746
+ 2, port_spec=_pf.virtual_port_spec(classification="electrical"), name="Differentiator"
1747
+ )
1748
+ comp.properties.__thumbnail__ = "electrical_differential"
1749
+ return comp
1750
+
1751
+
1752
+ @_pf.parametric_component
1753
+ def integrator(
1754
+ *,
1755
+ scale: float = 1.0,
1756
+ start_value: _pft.FieldAmplitude = 0.0,
1757
+ limits: _pft.annotate(_Sequence[_pft.FieldAmplitude | None], minItems=2, maxItems=2) = (
1758
+ None,
1759
+ None,
1760
+ ),
1761
+ reset_trigger: _typ.Literal["fall", "rise", "both"] = "rise",
1762
+ reset_tolerance: float = 0.0,
1763
+ ):
1764
+ r"""Integral of the input signal.
1765
+
1766
+ Args:
1767
+ scale: Output scaling factor.
1768
+ start_value: Starting output value after reset.
1769
+ limits: Output value limits.
1770
+ reset_trigger: Type of edge used for triggering a reset.
1771
+ reset_tolerance: Value change tolerance for triggering a reset.
1772
+ """
1773
+ model = _PassThroughModel()
1774
+ model.time_stepper = _pf.IntegralTimeStepper(
1775
+ scale=scale,
1776
+ start_value=start_value,
1777
+ limits=limits,
1778
+ output_port="E2",
1779
+ reset_port="E1",
1780
+ reset_trigger=reset_trigger,
1781
+ reset_tolerance=reset_tolerance,
1782
+ )
1783
+ comp = model.black_box_component(
1784
+ (0, -90, 180),
1785
+ port_spec=_pf.virtual_port_spec(classification="electrical"),
1786
+ name="Integrator",
1787
+ )
1788
+ comp.properties.__thumbnail__ = "electrical_integral"
1789
+ return comp
1790
+
1791
+
1416
1792
  if __name__ == "__main__":
1417
1793
  d = dict(locals())
1418
1794
 
@@ -6,9 +6,7 @@ from typing import Any, Literal
6
6
  import numpy
7
7
  import tidy3d
8
8
 
9
- from .analytic_models import WaveguideModel
10
9
  from .cache import cache_s_matrix
11
- from .eme_model import EMEModel
12
10
  from .extension import (
13
11
  Z_INF,
14
12
  Component,
@@ -24,37 +22,9 @@ from .extension import (
24
22
  frequency_classification,
25
23
  snap_to_grid,
26
24
  )
27
- from .tidy3d_model import _ModeSolverRunner
28
- from .utils import C_0
29
-
30
-
31
- def _gather_status(*runners: Any) -> dict[str, Any]:
32
- """Create an overall status based on a collection of runners."""
33
- num_tasks = 0
34
- progress = 0
35
- message = "success"
36
- tasks = {}
37
- for task in runners:
38
- task_status = (
39
- {"progress": 100, "message": "success"} if isinstance(task, SMatrix) else task.status
40
- )
41
- inner_tasks = task_status.get("tasks", {})
42
- tasks.update(inner_tasks)
43
- task_weight = max(1, len(inner_tasks))
44
- num_tasks += task_weight
45
- if message != "error":
46
- if task_status["message"] == "error":
47
- message = "error"
48
- elif task_status["message"] == "running":
49
- message = "running"
50
- progress += task_weight * task_status["progress"]
51
- elif task_status["message"] == "success":
52
- progress += task_weight * 100
53
- if message == "running":
54
- progress /= num_tasks
55
- else:
56
- progress = 100
57
- return {"progress": progress, "message": message, "tasks": tasks}
25
+ from .models.analytic import WaveguideModel
26
+ from .models.tidy3d import EMEModel, _ModeSolverRunner
27
+ from .utils import C_0, _angles_equal
58
28
 
59
29
 
60
30
  def _reference_ports(component, level, cache):
@@ -131,11 +101,6 @@ def _validate_query(
131
101
  return tuple(valid_key)
132
102
 
133
103
 
134
- def _compare_angles(a: float, b: float) -> bool:
135
- r = (a - b) % 360
136
- return r <= 1e-12 or 360 - r <= 1e-12
137
-
138
-
139
104
  # Return a flattening key (for caching) if flattening is required, and
140
105
  # a bool indicating whether phase correction is required
141
106
  def _analyze_transform(
@@ -153,7 +118,7 @@ def _analyze_transform(
153
118
  )
154
119
 
155
120
  translated = not numpy.allclose(reference.origin, (0, 0), atol=1e-12)
156
- rotated = not _compare_angles(reference.rotation, 0)
121
+ rotated = not _angles_equal(reference.rotation, 0)
157
122
 
158
123
  if not uniform and (translated or rotated):
159
124
  return (tuple(reference.origin.tolist()), reference.rotation, reference.x_reflection), None
@@ -197,7 +162,7 @@ def _analyze_transform(
197
162
  )
198
163
 
199
164
  if (fully_anisotropic and rotated) or (
200
- not in_plane_isotropic and rotated and not _compare_angles(reference.rotation, 180)
165
+ not in_plane_isotropic and rotated and not _angles_equal(reference.rotation, 180)
201
166
  ):
202
167
  return (None, reference.rotation, reference.x_reflection), None
203
168
 
Binary file
@@ -13,7 +13,7 @@ from fastapi.responses import StreamingResponse as _sr
13
13
  from fastapi.staticfiles import StaticFiles as _sf
14
14
  from fastapi.templating import Jinja2Templates as _j2
15
15
 
16
- import photonforge as _pf
16
+ from .extension import __version__
17
17
 
18
18
 
19
19
  class LiveViewer:
@@ -38,7 +38,7 @@ class LiveViewer:
38
38
  self.app = _f.FastAPI(
39
39
  title="LiveViewer server",
40
40
  description="PhotonForge LiveViewer server",
41
- version=_pf.__version__,
41
+ version=__version__,
42
42
  )
43
43
 
44
44
  self.app.add_middleware(