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,353 @@
1
+ from collections.abc import Sequence
2
+
3
+ import numpy
4
+ from numpy.random import SeedSequence, default_rng
5
+
6
+ from ..extension import (
7
+ Component,
8
+ TimeStepper,
9
+ register_time_stepper_class,
10
+ )
11
+ from ..typing import (
12
+ Frequency,
13
+ Gain,
14
+ NonNegativeInt,
15
+ Power_dBm,
16
+ TimeDelay,
17
+ annotate,
18
+ )
19
+ from ..utils import K_B, H
20
+
21
+
22
+ class OpticalAmplifierTimeStepper(TimeStepper):
23
+ r"""Time-stepper for a unidirectional optical amplifier.
24
+
25
+ This model implements a simple optical amplifier with a constant gain
26
+ and amplified spontaneous emission (ASE) noise. The ASE noise power is
27
+ determined by the amplifier's gain and noise figure.
28
+
29
+ Notes:
30
+ ASE noise is modeled as a complex Gaussian random process. Its one-sided
31
+ power spectral density (PSD) is:
32
+
33
+ .. math::
34
+
35
+ S_\text{ASE} &= n_{sp} (10^\frac{g}{10} - 1) h\nu
36
+
37
+ n_\text{sp} &= \frac{10^\frac{\text{NF}}{10}}{2}
38
+
39
+ in which :math:`g` is the power gain in dB, :math:`h\nu` is the photon
40
+ energy, and :math:`n_\text{sp}` is the spontaneous emission factor,
41
+ derived from the noise figure.
42
+
43
+ The discrete-time simulation represents a spectral slice of width
44
+ :math:`1/\Delta t` centered at the carrier frequency. To conserve the
45
+ total physical noise energy in this slice, the noise variance per sample
46
+ is:
47
+
48
+ .. math:: \sigma^2 = \frac{S_\text{ASE}}{\Delta t}
49
+
50
+ This corresponds to integrating the optical PSD over the full simulation
51
+ bandwidth.
52
+
53
+ Args:
54
+ gain: The amplifier's power gain.
55
+ noise_figure: The amplifier's noise figure (NF). If ``None``, noise
56
+ is disabled.
57
+ ports: Input and output port names. If not set, the *sorted* list of
58
+ port names from the component is used.
59
+ seed: Random number generator seed to ensure reproducibility.
60
+
61
+ Reference:
62
+ Agrawal, G. P. (2010). *Fiber-Optic Communication Systems*. Wiley.
63
+ """
64
+
65
+ def __init__(
66
+ self,
67
+ *,
68
+ gain: Gain = 0,
69
+ noise_figure: Gain | None = None,
70
+ ports: annotate(Sequence[str], minItems=2, maxItems=2) | None = None,
71
+ seed: NonNegativeInt | None = None,
72
+ ):
73
+ super().__init__(gain=gain, noise_figure=noise_figure, ports=ports, seed=seed)
74
+
75
+ def setup_state(
76
+ self,
77
+ *,
78
+ component: Component,
79
+ time_step: TimeDelay,
80
+ carrier_frequency: Frequency,
81
+ **kwargs,
82
+ ):
83
+ """Initialize internal state.
84
+
85
+ Args:
86
+ component: Component representing amplifier.
87
+ time_step: The interval between time steps (in seconds).
88
+ carrier_frequency: The carrier frequency used to construct the time
89
+ stepper. The carrier should be omitted from the input signals, as
90
+ it is handled automatically by the time stepper.
91
+ kwargs: Unused.
92
+ """
93
+ p = self.parametric_kwargs
94
+
95
+ ports = p["ports"]
96
+ component_ports = sorted(component.select_ports("optical"))
97
+ if ports is None:
98
+ ports = component_ports
99
+ if len(ports) != 2:
100
+ raise RuntimeError(
101
+ "OpticalAmplifierTimeStepper can only be used in components with 2 optical "
102
+ "ports."
103
+ )
104
+ elif len(ports) != 2:
105
+ raise ValueError("'Argument 'ports' must be a sequence of 2 port names.")
106
+ elif not all(name in component_ports for name in ports):
107
+ raise RuntimeError(
108
+ f"Not all port names defined in OpticalAmplifierTimeStepper ({ports!r}) match the "
109
+ f"optical port names in component '{component.name}' ({component_ports!r})."
110
+ )
111
+
112
+ self.keys = tuple(p + "@0" for p in ports)
113
+
114
+ g = 10.0 ** (p["gain"] / 10.0)
115
+ nf = p["noise_figure"]
116
+ n_sp = 0.0 if nf is None else (0.5 * 10.0 ** (nf / 10.0))
117
+ s_ase = n_sp * (g - 1.0) * H * carrier_frequency
118
+
119
+ self._gain = g**0.5
120
+ self._stdev = (0.5 * s_ase / time_step) ** 0.5
121
+ self._seed = SeedSequence() if p["seed"] is None else p["seed"]
122
+ self.reset()
123
+
124
+ def reset(self):
125
+ """Reset internal state."""
126
+ self._rng = default_rng(self._seed)
127
+ self._sample_noise()
128
+
129
+ def _sample_noise(self):
130
+ self._ase = self._rng.normal(0, self._stdev) + 1j * self._rng.normal(0, self._stdev)
131
+
132
+ def step_single(
133
+ self,
134
+ inputs: numpy.ndarray,
135
+ outputs: numpy.ndarray,
136
+ time_index: int,
137
+ update_state: bool,
138
+ shutdown: bool,
139
+ ) -> None:
140
+ """Take a single time step on the given inputs.
141
+
142
+ Args:
143
+ inputs: Input values at the current time step. Must be a 1D array of
144
+ complex values ordered
145
+ according to :attr:`keys`.
146
+ outputs: Pre-allocated output array where results will be stored.
147
+ Same size and type as ``inputs``.
148
+ time_index: Time series index for the current input.
149
+ update_state: Whether to update the internal stepper state.
150
+ shutdown: Whether this is the last call to the single stepping
151
+ function for the provided :class:`TimeSeries`.
152
+ """
153
+ outputs[1] = self._ase + self._gain * inputs[0]
154
+ if update_state:
155
+ self._sample_noise()
156
+
157
+
158
+ class ElectricalAmplifierTimeStepper(TimeStepper):
159
+ r"""Time-stepper for a broadband amplifier or modulator dirver.
160
+
161
+ This model implements an electrical amplifier with linear gain, noise,
162
+ bandwidth limiting, port reflections, and common nonlinear effects: 1 dB
163
+ compression, third-order intercept point (IP3), and saturation.
164
+
165
+ Args:
166
+ gain: The amplifier's power gain.
167
+ f_3dB: -3 dB frequency cutoff for bandwidth limiting.
168
+ saturation_power: Output saturation power.
169
+ compression_power: 1 dB compression power.
170
+ ip3: Third order intercept point.
171
+ noise_figure: The amplifier's noise figure (NF). If ``None``,
172
+ noise is disabled.
173
+ r0: Reflection coefficient for the input port.
174
+ r1: Reflection coefficient for the output port.
175
+ ports: Input and output port names. If not set, the *sorted* list of
176
+ port names from the component is used.
177
+ seed: Random number generator seed to ensure reproducibility.
178
+
179
+ Notes:
180
+ The forward path signal is processed through the following stages at
181
+ each time step:
182
+
183
+ 1. Low-pass filter for bandwidth limiting:
184
+
185
+ .. math::
186
+
187
+ y_0\![n] &= \alpha y_0\![n-1] + (1-\alpha) G_a a_\text{in}
188
+
189
+ \alpha &= e^{-2\pi f_\text{3dB} \Delta t}
190
+
191
+ G_a &= 10^\frac{G_\text{dB}}{20}
192
+
193
+ 2. Soft-knee compression:
194
+
195
+ .. math::
196
+
197
+ y_1 &= y_0 \left( 1 + \frac{c_s}{P_\text{1dB}} y_0^2
198
+ \right)^{-\frac{1}{2}}
199
+
200
+ c_s &= 10^\frac{1}{10} - 1
201
+
202
+ 3. Cubic IP3 nonlinearity:
203
+
204
+ .. math::
205
+
206
+ y_2 = y_1 \left(1 - \frac{1}{P_\text{IP3}} y_1^2\right)
207
+
208
+ 4. Smooth saturation: A `tanh` function smoothly clamps the output
209
+ amplitude to approach :math:`\sqrt{P_\text{sat}}`.
210
+
211
+ 5. Additive noise: if enabled, white Gaussian noise is added to the
212
+ output. The noise power spectral density is derived from the
213
+ noise figure :math:`F` as :math:`S_P = k_B T_0 G_\text{dB} F`.
214
+ """
215
+
216
+ def __init__(
217
+ self,
218
+ *,
219
+ gain: Gain = 0,
220
+ f_3dB: annotate(Frequency | None, label="f 3dB") = None,
221
+ saturation_power: Power_dBm | None = None,
222
+ compression_power: Power_dBm | None = None,
223
+ ip3: annotate(Power_dBm | None, label="IP3") = None,
224
+ noise_figure: Gain | None = None,
225
+ r0: annotate(float, minimum=-1, maximum=1) = 0,
226
+ r1: annotate(float, minimum=-1, maximum=1) = 0,
227
+ ports: annotate(Sequence[str], minItems=2, maxItems=2) | None = None,
228
+ seed: NonNegativeInt | None = None,
229
+ ):
230
+ super().__init__(
231
+ gain=gain,
232
+ f_3dB=f_3dB,
233
+ saturation_power=saturation_power,
234
+ compression_power=compression_power,
235
+ ip3=ip3,
236
+ noise_figure=noise_figure,
237
+ r0=r0,
238
+ r1=r1,
239
+ ports=ports,
240
+ seed=seed,
241
+ )
242
+
243
+ def setup_state(
244
+ self,
245
+ *,
246
+ component: Component,
247
+ time_step: TimeDelay,
248
+ **kwargs,
249
+ ):
250
+ """Initialize internal state.
251
+
252
+ Args:
253
+ component: Component representing the amplifier.
254
+ time_step: The interval between time steps (in seconds).
255
+ kwargs: Unused.
256
+ """
257
+ p = self.parametric_kwargs
258
+
259
+ ports = p["ports"]
260
+ component_ports = sorted(component.select_ports("electrical"))
261
+ if ports is None:
262
+ ports = component_ports
263
+ if len(ports) != 2:
264
+ raise RuntimeError(
265
+ "ElectricalAmplifierTimeStepper can only be used in components with 2 "
266
+ "electrical ports."
267
+ )
268
+ elif len(ports) != 2:
269
+ raise ValueError("'Argument 'ports' must be a sequence of 2 port names.")
270
+ elif not all(name in component_ports for name in ports):
271
+ raise RuntimeError(
272
+ f"Not all port names defined in ElectricalAmplifierTimeStepper ({ports!r}) match "
273
+ f"the electrical port names in component '{component.name}' ({component_ports!r})."
274
+ )
275
+
276
+ self.keys = tuple(p + "@0" for p in ports)
277
+
278
+ self._r0 = p["r0"]
279
+ self._r1 = p["r1"]
280
+
281
+ f_3dB = p["f_3dB"]
282
+ self._alpha = 0.0 if f_3dB is None else numpy.exp(-2 * numpy.pi * f_3dB * time_step)
283
+
284
+ g = 10.0 ** (p["gain"] / 10.0)
285
+ self._gain = (1.0 - self._alpha) * g**0.5
286
+
287
+ p1dB = p["compression_power"]
288
+ self._c_1dB = 0.0 if p1dB is None else ((10.0**0.1 - 1.0) * 10.0 ** (p1dB / -10.0 + 3.0))
289
+
290
+ ip3 = p["ip3"]
291
+ self._c_ip3 = 0.0 if ip3 is None else (10.0 ** (ip3 / -10.0 + 3.0))
292
+
293
+ p_sat = p["saturation_power"]
294
+ self._sat = 0.0 if p_sat is None else (10.0 ** (p_sat / 20.0 - 1.5))
295
+
296
+ T_0 = 290 # K
297
+ nf = p["noise_figure"]
298
+ self._stdev = (
299
+ 0.0 if nf is None else (0.5 * K_B * T_0 * g * 10 ** (nf / 10.0) / time_step) ** 0.5
300
+ )
301
+ self._seed = SeedSequence() if p["seed"] is None else p["seed"]
302
+
303
+ self.reset()
304
+
305
+ def reset(self):
306
+ """Reset internal state."""
307
+ self._y = 0.0
308
+ self._rng = default_rng(self._seed)
309
+ self._sample_noise()
310
+
311
+ def _sample_noise(self):
312
+ self._noise = self._rng.normal(0, self._stdev)
313
+
314
+ def step_single(
315
+ self,
316
+ inputs: numpy.ndarray,
317
+ outputs: numpy.ndarray,
318
+ time_index: int,
319
+ update_state: bool,
320
+ shutdown: bool,
321
+ ) -> None:
322
+ """Take a single time step on the given inputs.
323
+
324
+ Args:
325
+ inputs: Input values at the current time step. Must be a 1D array of
326
+ complex values ordered
327
+ according to :attr:`keys`.
328
+ outputs: Pre-allocated output array where results will be stored.
329
+ Same size and type as ``inputs``.
330
+ time_index: Time series index for the current input.
331
+ update_state: Whether to update the internal stepper state.
332
+ shutdown: Whether this is the last call to the single stepping
333
+ function for the provided :class:`TimeSeries`.
334
+ """
335
+ x0 = inputs[0].real
336
+ x1 = inputs[1].real
337
+
338
+ y0 = self._alpha * self._y + self._gain * x0
339
+ y = y0 / (1.0 + self._c_1dB * y0**2) ** 0.5
340
+ y = y * max(0.0, (1.0 - self._c_ip3 * y**2))
341
+ if self._sat > 0.0:
342
+ y = self._sat * numpy.tanh(y / self._sat)
343
+
344
+ outputs[0] = self._r0 * x0
345
+ outputs[1] = self._r1 * x1 + self._noise + y
346
+
347
+ if update_state:
348
+ self._y = y0
349
+ self._sample_noise()
350
+
351
+
352
+ register_time_stepper_class(OpticalAmplifierTimeStepper)
353
+ register_time_stepper_class(ElectricalAmplifierTimeStepper)
@@ -1,7 +1,21 @@
1
1
  import numpy
2
2
 
3
- from .extension import TimeStepper, register_time_stepper_class
4
- from .typing import TimeDelay
3
+ from ..extension import (
4
+ Component,
5
+ Interpolator,
6
+ TimeStepper,
7
+ register_time_stepper_class,
8
+ )
9
+ from ..typing import (
10
+ Coordinate,
11
+ Frequency,
12
+ Loss,
13
+ PropagationLoss,
14
+ TimeDelay,
15
+ )
16
+ from ..utils import C_0, route_length
17
+
18
+ _zero = Interpolator([0], [0])
5
19
 
6
20
 
7
21
  class _ArrayDelayBuffer:
@@ -224,4 +238,179 @@ class DelayedTimeStepper(TimeStepper):
224
238
  numpy.copyto(outputs, self._temp_output_buffer)
225
239
 
226
240
 
241
+ class AnalyticWaveguideTimeStepper(TimeStepper):
242
+ r"""Analytic waveguide tiem-stepper with power-dependent effects.
243
+
244
+ This model simulates a two-port optical waveguide where the effective
245
+ refractive index is dynamically perturbed by power-dependent effects,
246
+ specifically free-carrier dispersion and thermal changes. The total
247
+ effective index at any time is the sum of the baseline index and the
248
+ dynamic perturbations:
249
+
250
+ .. math::
251
+
252
+ &n(t) = n_\text{eff} + n_\text{fc}(t) + n_\text{th}(t)
253
+
254
+ &\tau_\text{fc} \frac{{\rm d}n_\text{fc}}{{\rm d}t}
255
+ + n_\text{fc}(t) = \Delta n_\text{fc}(P(t))
256
+
257
+ &\tau_\text{th} \frac{{\rm d}n_\text{th}}{{\rm d}t}
258
+ + n_\text{th}(t) = \Delta n_\text{th}(P(t))
259
+
260
+ in which :math:`\Delta n_\text{fc}` and :math:`\Delta n_\text{th}` are
261
+ the steady-state index changes corresponding to the instantaneous
262
+ optical power :math:`P(t)` due to free-carrier and thermal effects,
263
+ respectively.
264
+
265
+ Args:
266
+ n_eff: Effective refractive index (loss can be included here by
267
+ using complex values).
268
+ length: Length of the waveguide. If not provided, the length is
269
+ measured by :func:`route_length` or ports distance.
270
+ propagation_loss: Propagation loss.
271
+ n_group: Group index of the optical mode, used to calculate delay.
272
+ tau_fc: Time constant for the free-carrier effects. If zero,
273
+ free-carrier effects are disabled.
274
+ dn_fc: Power-dependent, steady-state index variation due to
275
+ free-carrier effects.
276
+ tau_th: Time constant for the thermal effects. If zero, thermal
277
+ effects are disabled.
278
+ dn_th: Power-dependent, steady-state index variation due to thermal
279
+ effects.
280
+
281
+ Notes:
282
+ The group delay :math:`n_g \ell / c_0` is implemented as a fixed
283
+ multiple of the time step.
284
+
285
+ This model uses a lumped element approximation by assuming a uniform
286
+ effect along the waveguide length.
287
+ """
288
+
289
+ def __init__(
290
+ self,
291
+ *,
292
+ n_eff: complex,
293
+ length: Coordinate | None = None,
294
+ propagation_loss: PropagationLoss = 0.0,
295
+ extra_loss: Loss = 0.0,
296
+ n_group: float = 0,
297
+ tau_fc: TimeDelay = 0,
298
+ dn_fc: Interpolator = _zero,
299
+ tau_th: TimeDelay = 0,
300
+ dn_th: Interpolator = _zero,
301
+ ):
302
+ super().__init__(
303
+ n_eff=n_eff,
304
+ length=length,
305
+ propagation_loss=propagation_loss,
306
+ extra_loss=extra_loss,
307
+ n_group=n_group,
308
+ tau_fc=tau_fc,
309
+ dn_fc=dn_fc,
310
+ tau_th=tau_th,
311
+ dn_th=dn_th,
312
+ )
313
+
314
+ def setup_state(
315
+ self,
316
+ *,
317
+ component: Component,
318
+ time_step: TimeDelay,
319
+ carrier_frequency: Frequency,
320
+ **kwargs,
321
+ ):
322
+ """Initialize internal state.
323
+
324
+ Args:
325
+ component: Component representing the laser source.
326
+ time_step: The interval between time steps (in seconds).
327
+ carrier_frequency: The carrier frequency used to construct the time
328
+ stepper. The carrier should be omitted from the input signals, as
329
+ it is handled automatically by the time stepper.
330
+ kwargs: Unused.
331
+ """
332
+ ports = sorted(component.select_ports("optical"))
333
+ if len(ports) != 2:
334
+ raise RuntimeError(
335
+ "AnalyticWaveguideTimeStepper can only be used in components with 2 optical ports."
336
+ )
337
+ self.keys = (ports[0] + "@0", ports[1] + "@0")
338
+
339
+ p = self.parametric_kwargs
340
+
341
+ length = p["length"]
342
+ if length is None:
343
+ length = 0
344
+ port0 = component.ports[ports[0]]
345
+ port1 = component.ports[ports[1]]
346
+ for _, _, layer in port0.spec.path_profiles_list():
347
+ length = max(length, route_length(component, layer))
348
+ if length <= 0:
349
+ length = numpy.sqrt(numpy.sum((port0.center - port1.center) ** 2))
350
+
351
+ self._alpha_fc = min(1.0, time_step / p["tau_fc"]) if p["tau_fc"] > 0 else 0.0
352
+ self._alpha_th = min(1.0, time_step / p["tau_th"]) if p["tau_th"] > 0 else 0.0
353
+
354
+ self._n_eff = p["n_eff"]
355
+ self._dn_fc_interp = p["dn_fc"]
356
+ self._dn_th_interp = p["dn_th"]
357
+
358
+ self._phi0 = 2.0 * numpy.pi * carrier_frequency * length / C_0
359
+ self._gain = 10.0 ** ((length * p["propagation_loss"] + p["extra_loss"]) / -20.0)
360
+
361
+ self._delay = max(0, int(numpy.round(p["n_group"] * length / C_0 / time_step)))
362
+ self._buffer = numpy.empty((self._delay, 2), dtype=complex)
363
+
364
+ self.reset()
365
+
366
+ def reset(self):
367
+ """Reset internal state."""
368
+ self._dn_fc = 0.0
369
+ self._dn_th = 0.0
370
+ self._buffer[:, :] = 0.0j
371
+ self._index = 0
372
+
373
+ def step_single(
374
+ self,
375
+ inputs: numpy.ndarray,
376
+ outputs: numpy.ndarray,
377
+ time_index: int,
378
+ update_state: bool,
379
+ shutdown: bool,
380
+ ) -> None:
381
+ """Take a single time step on the given inputs.
382
+
383
+ Args:
384
+ inputs: Input values at the current time step. Must be a 1D array of
385
+ complex values ordered according to :attr:`keys`.
386
+ outputs: Pre-allocated output array where results will be stored.
387
+ Same size and type as ``inputs``.
388
+ time_index: Time series index for the current input.
389
+ update_state: Whether to update the internal stepper state.
390
+ shutdown: Whether this is the last call to the single stepping
391
+ function for the provided :class:`TimeSeries`.
392
+ """
393
+ if self._delay > 0:
394
+ a0, a1 = self._buffer[self._index]
395
+ else:
396
+ a0 = inputs[0]
397
+ a1 = inputs[1]
398
+
399
+ factor = self._gain * numpy.exp(1j * self._phi0 * (self._n_eff + self._dn_fc + self._dn_th))
400
+ outputs[0] = factor * a1
401
+ outputs[1] = factor * a0
402
+
403
+ if update_state:
404
+ powers = numpy.abs(inputs) ** 2
405
+ dn_fc0, dn_fc1 = self._dn_fc_interp(powers)
406
+ dn_th0, dn_th1 = self._dn_th_interp(powers)
407
+ self._dn_fc = (1.0 - self._alpha_fc) * self._dn_fc + self._alpha_fc * (dn_fc0 + dn_fc1)
408
+ self._dn_th = (1.0 - self._alpha_th) * self._dn_th + self._alpha_th * (dn_th0 + dn_th1)
409
+
410
+ if self._delay > 0:
411
+ self._buffer[self._index] = inputs
412
+ self._index = (self._index + 1) % self._delay
413
+
414
+
227
415
  register_time_stepper_class(DelayedTimeStepper)
416
+ register_time_stepper_class(AnalyticWaveguideTimeStepper)
@@ -6,10 +6,10 @@ from typing import Any
6
6
 
7
7
  import numpy
8
8
 
9
- from . import typing as pft
10
- from .cache import _mode_overlap_cache
11
- from .circuit_base import _gather_status, _process_component_netlist
12
- from .extension import (
9
+ from .. import typing as pft
10
+ from ..cache import _mode_overlap_cache
11
+ from ..circuit_base import _process_component_netlist
12
+ from ..extension import (
13
13
  Component,
14
14
  FiberPort,
15
15
  GaussianPort,
@@ -18,7 +18,8 @@ from .extension import (
18
18
  config,
19
19
  register_time_stepper_class,
20
20
  )
21
- from .tidy3d_model import _align_and_overlap
21
+ from ..models.tidy3d import _align_and_overlap
22
+ from ..utils import _gather_status
22
23
 
23
24
 
24
25
  def _stepper(work_queue, *steppers):