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
|
@@ -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
|
|
4
|
-
|
|
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
|
|
10
|
-
from
|
|
11
|
-
from
|
|
12
|
-
from
|
|
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 .
|
|
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):
|