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.
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.0.dist-info → photonforge-1.3.2.dist-info}/METADATA +2 -2
  34. {photonforge-1.3.0.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.0.dist-info → photonforge-1.3.2.dist-info}/WHEEL +0 -0
  38. {photonforge-1.3.0.dist-info → photonforge-1.3.2.dist-info}/entry_points.txt +0 -0
  39. {photonforge-1.3.0.dist-info → photonforge-1.3.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,331 @@
1
+ from collections.abc import Sequence
2
+ from typing import Literal
3
+
4
+ import numpy
5
+
6
+ from ..extension import (
7
+ Component,
8
+ Expression,
9
+ TimeStepper,
10
+ register_time_stepper_class,
11
+ )
12
+ from ..typing import FieldAmplitude, annotate
13
+
14
+
15
+ class ExpressionTimeStepper(TimeStepper):
16
+ r"""Expression time stepper for electrical signals.
17
+
18
+ Args:
19
+ expressions: Dictionary of expression strings for all output ports,
20
+ indexed by name.
21
+
22
+ Example:
23
+ Example time stepper to be used on a component with 3 electrical
24
+ ports: "E0", "E1", "E2". The first has a 0.1 reflection coefficient,
25
+ the second has no output (zero, by default), and the third has a
26
+ non-linear combination of all 3 inputs.
27
+
28
+ >>> time_stepper = pf.ExpressionTimeStepper(
29
+ ... expressions={
30
+ ... "E0": "0.1 * E0",
31
+ ... "E2": "0.5 * E0 * E2 + abs(E1) * E1",
32
+ ... }
33
+ ... )
34
+
35
+ Note:
36
+ Expressions can use all operators supported by :class:`Expression`.
37
+ """
38
+
39
+ def __init__(self, *, expressions: dict[str, str]):
40
+ super().__init__(expressions=expressions)
41
+
42
+ def setup_state(self, *, component: Component, **kwargs):
43
+ """Initialize internal state.
44
+
45
+ Args:
46
+ component: Component representing the time stepper.
47
+ kwargs: Unused.
48
+ """
49
+ expressions = self.parametric_kwargs["expressions"]
50
+ component_ports = sorted(component.select_ports("electrical"))
51
+ if any(port not in component_ports for port in expressions):
52
+ raise RuntimeError(
53
+ f"Not all ports required by ExpressionTimeStepper were found in component "
54
+ f"{component.name!r}."
55
+ )
56
+ self.keys = tuple(port + "@0" for port in component_ports)
57
+ self.expression = Expression(
58
+ component_ports, [expressions.get(port, "0.0") for port in component_ports]
59
+ )
60
+
61
+ def step_single(
62
+ self,
63
+ inputs: numpy.ndarray,
64
+ outputs: numpy.ndarray,
65
+ time_index: int,
66
+ update_state: bool,
67
+ shutdown: bool,
68
+ ) -> None:
69
+ """Take a single time step on the given inputs.
70
+
71
+ Args:
72
+ inputs: Input values at the current time step. Must be a 1D array of
73
+ complex values ordered
74
+ according to :attr:`keys`.
75
+ outputs: Pre-allocated output array where results will be stored.
76
+ Same size and type as ``inputs``.
77
+ time_index: Time series index for the current input.
78
+ update_state: Whether to update the internal stepper state.
79
+ shutdown: Whether this is the last call to the single stepping
80
+ function for the provided :class:`TimeSeries`.
81
+ """
82
+ numpy.copyto(outputs, self.expression(*inputs.real))
83
+
84
+
85
+ class DifferentialTimeStepper(TimeStepper):
86
+ r"""Time stepper for differentiating electrical signals in time.
87
+
88
+ The "backwards" scheme, implements:
89
+
90
+ .. math:: y_k = s \frac{x_k - x_{k-1}}{\Delta t}
91
+
92
+ whereas the "central" scheme uses:
93
+
94
+ .. math:: y_k = s \frac{x_k - x_{k-2}}{2 \Delta t}
95
+
96
+ Args:
97
+ scheme: Differentiation scheme.
98
+ scale: Output scaling factor :math:`s`.
99
+ output_port: Name of the port used as output. If ``None``, the last
100
+ port is used (in the sorted list of ports).
101
+ """
102
+
103
+ def __init__(
104
+ self,
105
+ *,
106
+ scheme: Literal["backwards", "central"] = "backwards",
107
+ scale: annotate(float, units="s") = 1.0,
108
+ output_port: str | None = None,
109
+ ):
110
+ super().__init__(scheme=scheme, scale=scale, output_port=output_port)
111
+
112
+ def setup_state(self, *, time_step: float, component: Component, **kwargs):
113
+ """Initialize internal state.
114
+
115
+ Args:
116
+ time_step: The interval between time steps (in seconds).
117
+ component: Component representing the time stepper.
118
+ kwargs: Unused.
119
+ """
120
+ p = self.parametric_kwargs
121
+ component_ports = sorted(component.select_ports("electrical"))
122
+ if len(component_ports) != 2:
123
+ raise RuntimeError(
124
+ f"DifferentialTimeStepper can only be used on components with 2 electrical "
125
+ f"ports. Component {component.name!r} has {len(component_ports)}."
126
+ )
127
+
128
+ output_port = p["output_port"]
129
+ if output_port == component_ports[0]:
130
+ component_ports = (component_ports[1], component_ports[0])
131
+ elif output_port is not None and output_port != component_ports[1]:
132
+ raise RuntimeError(
133
+ f"Port {output_port!r} required by DifferentialTimeStepper was not found in "
134
+ f"component {component.name!r}."
135
+ )
136
+ self.keys = tuple(p + "@0" for p in component_ports)
137
+
138
+ scheme = p["scheme"]
139
+ if scheme not in ("backwards", "central"):
140
+ raise ValueError("DifferentialTimeStepper 'scheme' must be 'backwards' or 'central'.")
141
+ self._buffer = numpy.empty(1 if scheme == "backwards" else 2)
142
+ self._scale = p["scale"] / (self._buffer.size * time_step)
143
+
144
+ self.reset()
145
+
146
+ def reset(self):
147
+ self._buffer[:] = 0.0
148
+
149
+ def step_single(
150
+ self,
151
+ inputs: numpy.ndarray,
152
+ outputs: numpy.ndarray,
153
+ time_index: int,
154
+ update_state: bool,
155
+ shutdown: bool,
156
+ ) -> None:
157
+ """Take a single time step on the given inputs.
158
+
159
+ Args:
160
+ inputs: Input values at the current time step. Must be a 1D array of
161
+ complex values ordered
162
+ according to :attr:`keys`.
163
+ outputs: Pre-allocated output array where results will be stored.
164
+ Same size and type as ``inputs``.
165
+ time_index: Time series index for the current input.
166
+ update_state: Whether to update the internal stepper state.
167
+ shutdown: Whether this is the last call to the single stepping
168
+ function for the provided :class:`TimeSeries`.
169
+ """
170
+ x = inputs[0].real
171
+ outputs[1] = self._scale * (x - self._buffer[0])
172
+
173
+ if update_state:
174
+ if self._buffer.size == 1:
175
+ self._buffer[0] = x
176
+ else:
177
+ self._buffer[0] = self._buffer[1]
178
+ self._buffer[1] = x
179
+
180
+
181
+ class IntegralTimeStepper(TimeStepper):
182
+ r"""Time stepper for integrating electrical signals in time.
183
+
184
+ .. math:: y_k = y_{k - 1} + s x_k \Delta t
185
+
186
+ Args:
187
+ scale: Input scaling factor :math:`s`.
188
+ start_value: Starting output value after reset.
189
+ limits: Output value limits.
190
+ output_port: Name of the port used as output. If ``None``, the last
191
+ port is used (in the sorted list of ports).
192
+ reset_port: Name of the port used as reset signal. If ``None``, the
193
+ reset feature is disabled.
194
+ reset_trigger: Type of edge used for triggering a reset.
195
+ reset_tolerance: Value change tolerance for triggering a reset.
196
+ """
197
+
198
+ def __init__(
199
+ self,
200
+ *,
201
+ scale: annotate(float, units="s⁻¹") = 1.0,
202
+ start_value: FieldAmplitude = 0.0,
203
+ limits: annotate(Sequence[FieldAmplitude | None], minItems=2, maxItems=2) = (
204
+ None,
205
+ None,
206
+ ),
207
+ output_port: str | None = None,
208
+ reset_port: str | None = None,
209
+ reset_trigger: Literal["fall", "rise", "both"] = "rise",
210
+ reset_tolerance: float = 0.0,
211
+ ):
212
+ super().__init__(
213
+ scale=scale,
214
+ start_value=start_value,
215
+ limits=limits,
216
+ output_port=output_port,
217
+ reset_port=reset_port,
218
+ reset_trigger=reset_trigger,
219
+ reset_tolerance=reset_tolerance,
220
+ )
221
+
222
+ def setup_state(self, *, time_step: float, component: Component, **kwargs):
223
+ """Initialize internal state.
224
+
225
+ Args:
226
+ time_step: The interval between time steps (in seconds).
227
+ component: Component representing the time stepper.
228
+ kwargs: Unused.
229
+ """
230
+ p = self.parametric_kwargs
231
+ reset_port = p["reset_port"]
232
+ output_port = p["output_port"]
233
+
234
+ required_ports = 2 if reset_port is None else 3
235
+ component_ports = sorted(component.select_ports("electrical"))
236
+ if len(component_ports) != required_ports:
237
+ raise RuntimeError(
238
+ f"IntegralTimeStepper can only be used on components with {required_ports} "
239
+ f"electrical ports. Component {component.name!r} has {len(component_ports)}."
240
+ )
241
+ for required in (output_port, reset_port):
242
+ if required is not None and required not in component_ports:
243
+ raise RuntimeError(
244
+ f"Port {required!r} required by IntegralTimeStepper was not found in "
245
+ f"component {component.name!r}."
246
+ )
247
+
248
+ if reset_port is not None:
249
+ component_ports.remove(reset_port)
250
+ if reset_port == output_port:
251
+ raise ValueError("'output_port' and 'reset_port' cannot be the same.")
252
+
253
+ if output_port is None:
254
+ input_port, output_port = component_ports
255
+ else:
256
+ component_ports.remove(output_port)
257
+ input_port = component_ports[0]
258
+
259
+ if reset_port is None:
260
+ self.keys = (input_port + "@0", output_port + "@0")
261
+ else:
262
+ self.keys = (input_port + "@0", output_port + "@0", reset_port + "@0")
263
+
264
+ self._reset_trigger = p["reset_trigger"]
265
+ if self._reset_trigger not in ("fall", "rise", "both"):
266
+ raise ValueError(
267
+ "IntegralTimeStepper 'reset_trigger' must be 'fall', 'rise', or 'both'."
268
+ )
269
+
270
+ self._scale = p["scale"] * time_step
271
+ self._y0 = p["start_value"]
272
+ self._reset_tolerance = abs(p["reset_tolerance"])
273
+
274
+ self._y_lo, self._y_hi = p["limits"]
275
+ if self._y_lo is None:
276
+ self._y_lo = -numpy.inf
277
+ if self._y_hi is None:
278
+ self._y_hi = numpy.inf
279
+
280
+ self.reset()
281
+
282
+ def reset(self):
283
+ self._y = self._y0
284
+ self._r = 0.0
285
+
286
+ def step_single(
287
+ self,
288
+ inputs: numpy.ndarray,
289
+ outputs: numpy.ndarray,
290
+ time_index: int,
291
+ update_state: bool,
292
+ shutdown: bool,
293
+ ) -> None:
294
+ """Take a single time step on the given inputs.
295
+
296
+ Args:
297
+ inputs: Input values at the current time step. Must be a 1D array of
298
+ complex values ordered
299
+ according to :attr:`keys`.
300
+ outputs: Pre-allocated output array where results will be stored.
301
+ Same size and type as ``inputs``.
302
+ time_index: Time series index for the current input.
303
+ update_state: Whether to update the internal stepper state.
304
+ shutdown: Whether this is the last call to the single stepping
305
+ function for the provided :class:`TimeSeries`.
306
+ """
307
+ if len(self.keys) == 3:
308
+ r = inputs[2].real
309
+ else:
310
+ r = 0.0
311
+
312
+ edge = r - self._r
313
+ if len(self.keys) == 3 and (
314
+ (edge > self._reset_tolerance and self._reset_trigger != "fall")
315
+ or (-edge > self._reset_tolerance and self._reset_trigger != "rise")
316
+ ):
317
+ y = self._y0
318
+ else:
319
+ y = self._y + self._scale * inputs[0].real
320
+
321
+ y = min(self._y_hi, max(self._y_lo, y))
322
+ outputs[1] = y
323
+
324
+ if update_state:
325
+ self._y = y
326
+ self._r = r
327
+
328
+
329
+ register_time_stepper_class(ExpressionTimeStepper)
330
+ register_time_stepper_class(DifferentialTimeStepper)
331
+ register_time_stepper_class(IntegralTimeStepper)
@@ -5,9 +5,7 @@ from typing import Literal
5
5
 
6
6
  import numpy
7
7
 
8
- from .analytic_models import _port_with_x_section
9
- from .circuit_base import _gather_status
10
- from .extension import (
8
+ from ..extension import (
11
9
  Component,
12
10
  Interpolator,
13
11
  Port,
@@ -16,8 +14,9 @@ from .extension import (
16
14
  TimeStepper,
17
15
  register_time_stepper_class,
18
16
  )
19
- from .tidy3d_model import _isotropic_uniform, _ModeSolverRunner
20
- from .typing import (
17
+ from ..models.analytic import _port_with_x_section
18
+ from ..models.tidy3d import _isotropic_uniform, _ModeSolverRunner
19
+ from ..typing import (
21
20
  Coordinate,
22
21
  Frequency,
23
22
  Impedance,
@@ -27,7 +26,7 @@ from .typing import (
27
26
  TimeDelay,
28
27
  annotate,
29
28
  )
30
- from .utils import C_0, route_length
29
+ from ..utils import C_0, _gather_status, route_length
31
30
 
32
31
 
33
32
  def _impedance_monitor(time_stepper, runner):
@@ -220,10 +219,7 @@ class PhaseModTimeStepper(TimeStepper):
220
219
  "PhaseModTimeStepper can only be used in components with 2 optical and 1 "
221
220
  "electrical ports."
222
221
  )
223
- self._e = e_ports[0] + "@0"
224
- self._opt0 = ports[0] + "@0"
225
- self._opt1 = ports[1] + "@0"
226
- self.keys = (self._opt0, self._opt1, self._e)
222
+ self.keys = (ports[0] + "@0", ports[1] + "@0", e_ports[0] + "@0")
227
223
 
228
224
  p = self.parametric_kwargs
229
225
 
@@ -308,9 +304,7 @@ class PhaseModTimeStepper(TimeStepper):
308
304
  if update_state:
309
305
  self._v = v
310
306
  if self._delay > 0:
311
- a0 = inputs[0]
312
- a1 = inputs[1]
313
- self._buffer[self._index] = (a0, a1)
307
+ self._buffer[self._index] = inputs[:2]
314
308
  self._index = (self._index + 1) % self._delay
315
309
 
316
310
 
@@ -485,10 +479,7 @@ class TerminatedModTimeStepper(TimeStepper):
485
479
  f"Optical input port {port_name!r} not found in component {component.name!r}."
486
480
  )
487
481
  e_port = e_ports[0]
488
- self._e = e_port + "@0"
489
- self._opt0 = ports[0] + "@0"
490
- self._opt1 = ports[1] + "@0"
491
- self.keys = (self._opt0, self._opt1, self._e)
482
+ self.keys = (ports[0] + "@0", ports[1] + "@0", e_port + "@0")
492
483
 
493
484
  f_nyquist = 0.5 / time_step
494
485
  self._freqs = numpy.linspace(0, f_nyquist, p["fir_taps"])
@@ -784,9 +775,7 @@ class TerminatedModTimeStepper(TimeStepper):
784
775
  self._v[1:] = self._v[:-1]
785
776
  self._v[0] = v_in
786
777
  if self._delay > 0:
787
- a0 = inputs[0]
788
- a1 = inputs[1]
789
- self._buffer[self._index] = (a0, a1)
778
+ self._buffer[self._index] = inputs[:2]
790
779
  self._index = (self._index + 1) % self._delay
791
780
 
792
781
 
@@ -6,8 +6,8 @@ from typing import Any, Literal
6
6
 
7
7
  import numpy
8
8
 
9
- from .cache import _pole_residue_fit_cache
10
- from .extension import (
9
+ from ..cache import _pole_residue_fit_cache
10
+ from ..extension import (
11
11
  Component,
12
12
  PoleResidueMatrix,
13
13
  SMatrix,
@@ -19,7 +19,7 @@ from .extension import (
19
19
  pole_residue_fit,
20
20
  register_time_stepper_class,
21
21
  )
22
- from .typing import Frequency, NonNegativeInt, PositiveFloat, PositiveInt, TimeDelay
22
+ from ..typing import Frequency, NonNegativeInt, PositiveFloat, PositiveInt, TimeDelay
23
23
 
24
24
 
25
25
  class SMatrixTimeStepper(TimeStepper):
@@ -2,9 +2,8 @@ import numpy
2
2
  from numpy.random import SeedSequence, default_rng
3
3
  from scipy.signal import firwin2
4
4
 
5
- from .extension import Component, Interpolator, TimeStepper, register_time_stepper_class
6
- from .modulator_time_steppers import _get_impedance_runner
7
- from .typing import (
5
+ from ..extension import Component, Interpolator, TimeStepper, register_time_stepper_class
6
+ from ..typing import (
8
7
  Frequency,
9
8
  Impedance,
10
9
  NonNegativeFloat,
@@ -13,7 +12,8 @@ from .typing import (
13
12
  TimeDelay,
14
13
  annotate,
15
14
  )
16
- from .utils import Q as _Q
15
+ from ..utils import Q as _Q
16
+ from .modulator import _get_impedance_runner
17
17
 
18
18
  _PINK_TAPS = 2048
19
19
  _PINK_SAMPLES = 100
@@ -202,9 +202,7 @@ class PhotodiodeTimeStepper(TimeStepper):
202
202
  "PhotodiodeTimeStepper can only be used in components with 1 optical port and 1 "
203
203
  "electrical port."
204
204
  )
205
- self._port = ports[0] + "@0"
206
- self._e_port = e_ports[0] + "@0"
207
- self.keys = (self._port, self._e_port)
205
+ self.keys = (ports[0] + "@0", e_ports[0] + "@0")
208
206
 
209
207
  self._time_step = time_step
210
208
 
@@ -222,7 +220,7 @@ class PhotodiodeTimeStepper(TimeStepper):
222
220
  tau = p["current_time_constant"]
223
221
  if tau <= 0:
224
222
  tau = 100 * time_step
225
- self._current_factor = 1 - numpy.exp(-time_step / tau)
223
+ self._current_factor = 1.0 - numpy.exp(-time_step / tau)
226
224
 
227
225
  self._filter = None
228
226
  if p["filter_frequency"] > 0:
@@ -7,7 +7,7 @@ import numpy
7
7
  from numpy.random import SeedSequence, default_rng
8
8
  from scipy.integrate import solve_ivp
9
9
 
10
- from .extension import (
10
+ from ..extension import (
11
11
  Component,
12
12
  Interpolator,
13
13
  TimeStepper,
@@ -16,9 +16,9 @@ from .extension import (
16
16
  _prbs31,
17
17
  register_time_stepper_class,
18
18
  )
19
- from .modulator_time_steppers import _get_impedance_runner
20
- from .typing import (
19
+ from ..typing import (
21
20
  Angle,
21
+ FieldAmplitude,
22
22
  Fraction,
23
23
  Frequency,
24
24
  Impedance,
@@ -30,7 +30,8 @@ from .typing import (
30
30
  TimeDelay,
31
31
  annotate,
32
32
  )
33
- from .utils import C_0, H, Q
33
+ from ..utils import C_0, H, Q
34
+ from .modulator import _get_impedance_runner
34
35
 
35
36
  RIN = annotate(NonNegativeFloat, label="Relative Intensity Noise", units="1/Hz")
36
37
 
@@ -110,8 +111,7 @@ class CWLaserTimeStepper(TimeStepper):
110
111
  raise RuntimeError(
111
112
  "CWLaserTimeStepper can only be used in components with 1 optical port."
112
113
  )
113
- self._port = next(iter(ports)) + "@0"
114
- self.keys = (self._port,)
114
+ self.keys = (next(iter(ports)) + "@0",)
115
115
 
116
116
  p = self.parametric_kwargs
117
117
 
@@ -347,19 +347,24 @@ class DMLaserTimeStepper(TimeStepper):
347
347
  "PhotodiodeTimeStepper can only be used in components with 1 optical port and 1 "
348
348
  "electrical port."
349
349
  )
350
- self._port = ports[0] + "@0"
351
- self._e_port = e_ports[0] + "@0"
352
- self.keys = (self._port, self._e_port)
350
+ self.keys = (ports[0] + "@0", e_ports[0] + "@0")
353
351
 
354
352
  self._time_step = time_step
355
353
 
356
354
  p = self.parametric_kwargs
355
+
357
356
  self._r = complex(p["reflection"])
357
+
358
+ n_group = p["n_group"]
359
+ if n_group == 0.0:
360
+ raise RuntimeError("'n_group' cannot be zero.")
361
+ self._vg_a0 = C_0 * 1e-6 / n_group * p["differential_gain"]
362
+
358
363
  self._kp = float(
359
364
  (H * carrier_frequency * p["quantum_efficiency"] * p["active_region_volume"])
360
365
  / (2.0 * p["confinement_factor"] * p["photon_lifetime"])
361
366
  )
362
- self._vg_a0 = C_0 * 1e-6 / p["n_group"] * p["differential_gain"]
367
+
363
368
  self._tau_n = p["carrier_lifetime"]
364
369
  self._gamma = p["confinement_factor"]
365
370
  self._eps = p["gain_compression_factor"]
@@ -548,8 +553,7 @@ class OpticalPulseTimeStepper(TimeStepper):
548
553
  raise RuntimeError(
549
554
  "OpticalPulseTimeStepper can only be used in components with 1 optical port."
550
555
  )
551
- self._port = next(iter(ports)) + "@0"
552
- self.keys = (self._port,)
556
+ self.keys = (next(iter(ports)) + "@0",)
553
557
 
554
558
  repetition_rate = p["repetition_rate"]
555
559
  if repetition_rate > 0.5 / time_step:
@@ -747,8 +751,7 @@ class OpticalNoiseTimeStepper(TimeStepper):
747
751
  raise RuntimeError(
748
752
  "OpticalNoiseTimeStepper can only be used in components with 1 optical port."
749
753
  )
750
- self._port = next(iter(ports)) + "@0"
751
- self.keys = (self._port,)
754
+ self.keys = (next(iter(ports)) + "@0",)
752
755
 
753
756
  p = self.parametric_kwargs
754
757
 
@@ -835,8 +838,8 @@ class WaveformTimeStepper(TimeStepper):
835
838
  self,
836
839
  *,
837
840
  frequency: Frequency,
838
- amplitude: annotate(float, units="√W") = 1,
839
- offset: annotate(float, units="√W") = 0,
841
+ amplitude: FieldAmplitude = 1,
842
+ offset: FieldAmplitude = 0,
840
843
  start: Time | None = None,
841
844
  stop: Time | None = None,
842
845
  waveform: Literal["sine", "triangle", "trapezoid", "raised-cosine", "gaussian"] = "sine",
@@ -883,8 +886,7 @@ class WaveformTimeStepper(TimeStepper):
883
886
  raise RuntimeError(
884
887
  "WaveformTimeStepper can only be used in components with 1 electrical port."
885
888
  )
886
- self._port = next(iter(ports)) + "@0"
887
- self.keys = (self._port,)
889
+ self.keys = (next(iter(ports)) + "@0",)
888
890
 
889
891
  freq = p["frequency"]
890
892
  if freq > 0.5 / time_step:
photonforge/typing.py CHANGED
@@ -265,7 +265,12 @@ Voltage = annotate(float, units="V") #:
265
265
  Impedance = annotate(complex, units="Ω") #:
266
266
 
267
267
  Power = annotate(float, minimum=0, units="W") #:
268
+ Power_dBm = annotate(float, units="dBm") #:
268
269
 
270
+ FieldAmplitude = annotate(float, units="√W") #:
271
+ ComplexFieldAmplitude = annotate(complex, units="√W") #:
272
+
273
+ Gain = annotate(float, units="dB") #:
269
274
  Loss = annotate(float, minimum=0, units="dB") #:
270
275
  PropagationLoss = annotate(float, minimum=0, units="dB/μm") #:
271
276