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
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 .
|
|
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 .
|
|
82
|
-
from .
|
|
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 .
|
|
98
|
-
from .
|
|
99
|
-
from .
|
|
100
|
-
from .
|
|
101
|
-
from .
|
|
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 .
|
|
109
|
-
from .
|
|
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.
|
|
10
|
-
from photonforge.
|
|
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 =
|
|
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:
|
|
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 :
|
|
791
|
-
Maps mode index
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
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 :
|
|
877
|
-
Maps mode index
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
1349
|
-
offset: _pft.
|
|
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 =
|
|
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")),
|
|
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
|
|
photonforge/circuit_base.py
CHANGED
|
@@ -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 .
|
|
28
|
-
from .
|
|
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
|
|
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
|
|
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
|
photonforge/live_viewer.py
CHANGED
|
@@ -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
|
-
|
|
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=
|
|
41
|
+
version=__version__,
|
|
42
42
|
)
|
|
43
43
|
|
|
44
44
|
self.app.add_middleware(
|