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,400 @@
1
+ from collections.abc import Sequence
2
+ from typing import Literal
3
+
4
+ import numpy
5
+ from scipy import signal
6
+
7
+ from ..extension import (
8
+ Component,
9
+ Interpolator,
10
+ PoleResidueMatrix,
11
+ TimeDomainModel,
12
+ TimeStepper,
13
+ register_time_stepper_class,
14
+ )
15
+ from ..typing import (
16
+ Frequency,
17
+ Impedance,
18
+ Loss,
19
+ PositiveFloat,
20
+ PositiveInt,
21
+ TimeDelay,
22
+ annotate,
23
+ )
24
+ from .modulator import _get_impedance_runner
25
+
26
+
27
+ class FilterTimeStepper(TimeStepper):
28
+ """Filter time stepper for electrical signals
29
+
30
+ Args:
31
+ family: Filter family.
32
+ shape: Filter shape.
33
+ f_cutoff: Cutoff frequency for low- and high-pass filter shapes. For
34
+ ``family=="tunable_lp_rc"``, this can be an :class:`Interpolator`
35
+ to provide the dependency with the input voltage. For band-pass
36
+ and band-stop shapes, this is a 2-value sequence with low and high
37
+ cutoff frequencies.
38
+ order: Filter order.
39
+ ripple: Maximum ripple for Chebyshev filters.
40
+ window: Window specification for rectangular filters. Please consult
41
+ the help for ``scipy.signal.firwin`` for valid options.
42
+ a: Recursive (denominator) coefficients for a digital IIR filter.
43
+ b: Direct (numerator) coefficients for a digital FIR or IIR filter.
44
+ taps: Length of rectangular or Gaussian filters.
45
+ insertion_loss: Insertion loss added to the filter response.
46
+ ports: Input and output port names. If not set, the *sorted* list of
47
+ port names from the component is used.
48
+ z0: Characteristic impedance of the electrical port used to convert
49
+ the input control field amplitude to voltage for the
50
+ ``"tunable_lp_rc"`` family. If ``None``, derived from port
51
+ impedance, calculated by mode-solving, or set to 50 Ω.
52
+ mesh_refinement: Minimal number of mesh elements per wavelength used
53
+ for mode solving.
54
+ verbose: Flag setting the verbosity of mode solver runs.
55
+
56
+ Note:
57
+ Filtering is applied to input signals from the first port (sorted
58
+ alphabetically) and output on the second port. For the
59
+ ``"tunable_lp_rc"``, the third port contains the control signal.
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ *,
65
+ family: Literal[
66
+ "tunable_lp_rc",
67
+ "digital",
68
+ "rc",
69
+ "butterworth",
70
+ "bessel",
71
+ "cheby1",
72
+ "rectangular",
73
+ "gaussian",
74
+ ],
75
+ shape: Literal["lp", "hp", "bp", "bs"] = "lp",
76
+ f_cutoff: Frequency
77
+ | annotate(Sequence[Frequency], minItems=2, maxItems=2)
78
+ | Interpolator = 0,
79
+ order: PositiveInt = 1,
80
+ ripple: Loss = 0,
81
+ window: str
82
+ | tuple[str, float]
83
+ | tuple[str, float, float]
84
+ | tuple[str, Sequence[float]] = "hann",
85
+ a: Sequence[complex] = (1.0,),
86
+ b: Sequence[complex] = (),
87
+ taps: PositiveInt = 101,
88
+ insertion_loss: Loss = 0,
89
+ ports: annotate(Sequence[str], minItems=2, maxItems=2) | None = None,
90
+ z0: Impedance | Interpolator | None = None,
91
+ mesh_refinement: PositiveFloat | None = None,
92
+ verbose: bool = True,
93
+ ):
94
+ super().__init__(
95
+ family=family,
96
+ shape=shape,
97
+ f_cutoff=f_cutoff,
98
+ order=order,
99
+ ripple=ripple,
100
+ window=window,
101
+ a=a,
102
+ b=b,
103
+ taps=taps,
104
+ insertion_loss=insertion_loss,
105
+ ports=ports,
106
+ z0=z0,
107
+ mesh_refinement=mesh_refinement,
108
+ verbose=verbose,
109
+ )
110
+
111
+ def setup_state(
112
+ self,
113
+ *,
114
+ component: Component,
115
+ time_step: TimeDelay,
116
+ verbose: bool | None = None,
117
+ **kwargs,
118
+ ):
119
+ """Initialize internal state.
120
+
121
+ Args:
122
+ component: Component representing the filter.
123
+ time_step: The interval between time steps (in seconds).
124
+ kwargs: Unused.
125
+ """
126
+ p = self.parametric_kwargs
127
+ self._family = p["family"]
128
+
129
+ required_ports = 3 if self._family == "tunable_lp_rc" else 2
130
+
131
+ ports = p["ports"]
132
+ component_ports = sorted(component.select_ports("electrical"))
133
+ if ports is None:
134
+ ports = component_ports
135
+ if len(ports) != required_ports:
136
+ raise RuntimeError(
137
+ f"FilterTimeStepper of family {self._family!r} can only be used in components "
138
+ f"with {required_ports} electrical ports."
139
+ )
140
+ elif len(ports) != 2:
141
+ raise ValueError("'Argument 'ports' must be a sequence of 2 port names.")
142
+ elif not all(name in component_ports for name in ports):
143
+ raise RuntimeError(
144
+ f"Not all port names defined in FilterTimeStepper match the electrical port "
145
+ f"names in component '{component.name}'."
146
+ )
147
+ elif required_ports == 3:
148
+ component_ports.remove(ports[0])
149
+ component_ports.remove(ports[1])
150
+ ports = (*ports, component_ports[0])
151
+
152
+ self.keys = tuple(p + "@0" for p in ports)
153
+
154
+ self._gain = 10.0 ** (p["insertion_loss"] / -20.0)
155
+
156
+ f_nyquist = 0.5 / time_step
157
+
158
+ if self._family == "tunable_lp_rc":
159
+ self._time_step = time_step
160
+ self._f_cutoff = p["f_cutoff"]
161
+ if not isinstance(self._f_cutoff, Interpolator):
162
+ self._f_cutoff = Interpolator([0], [self._f_cutoff])
163
+
164
+ if _get_impedance_runner(self, component, ports[2], time_step, verbose) is not None:
165
+ return self
166
+
167
+ elif self._family == "digital":
168
+ a = numpy.asarray(p["a"], dtype=float).ravel()
169
+ b = numpy.asarray(p["b"], dtype=float).ravel()
170
+ if b.size < 1 or a.size < 1:
171
+ raise RuntimeError(
172
+ "Digital filter requires 'b' and 'a' coefficients of size greater than 0."
173
+ )
174
+ self._b = b / a[0]
175
+ self._a = a[1:] / a[0]
176
+ self._x = numpy.empty(b.size - 1, dtype=float)
177
+ self._y = numpy.empty(self._a.size, dtype=float)
178
+
179
+ elif self._family in ("rectangular", "gaussian"):
180
+ if self._family == "rectangular":
181
+ shape = p["shape"]
182
+ if shape == "lp":
183
+ b = signal.firwin(
184
+ p["taps"], p["f_cutoff"] / f_nyquist, pass_zero=True, window=p["window"]
185
+ )
186
+ elif shape == "hp":
187
+ b = signal.firwin(
188
+ p["taps"], p["f_cutoff"] / f_nyquist, pass_zero=False, window=p["window"]
189
+ )
190
+ elif shape == "bp":
191
+ f_low, f_high = p["f_cutoff"]
192
+ b = signal.firwin(
193
+ p["taps"],
194
+ [f_low / f_nyquist, f_high / f_nyquist],
195
+ pass_zero=False,
196
+ window=p["window"],
197
+ )
198
+ elif shape == "bs":
199
+ f_low, f_high = p["f_cutoff"]
200
+ b = signal.firwin(
201
+ p["taps"],
202
+ [f_low / f_nyquist, f_high / f_nyquist],
203
+ pass_zero=True,
204
+ window=p["window"],
205
+ )
206
+ else:
207
+ raise ValueError(f"Unrecognized filter shape {shape!r}.")
208
+
209
+ else:
210
+ shape = p["shape"]
211
+ taps = p["taps"]
212
+ k = numpy.arange(taps)
213
+ fs = 1.0 / time_step
214
+ fpos = k / taps * fs
215
+ f_signed = numpy.where(k <= taps // 2, fpos, fpos - fs)
216
+
217
+ def lp(freqs, f_cutoff, m):
218
+ return numpy.exp(-0.5 * numpy.log(2.0) * numpy.abs(freqs / f_cutoff) ** m)
219
+
220
+ def hp(freqs, f_cutoff, m):
221
+ non_zero = freqs != 0
222
+ transformed = numpy.zeros(freqs.size)
223
+ transformed[non_zero] = f_cutoff**2 / numpy.abs(freqs[non_zero])
224
+ h = lp(transformed, f_cutoff, m)
225
+ h[numpy.logical_not(non_zero)] = 0.0
226
+ return h
227
+
228
+ m = 2 * p["order"]
229
+ if shape == "lp":
230
+ h = lp(f_signed, p["f_cutoff"], m)
231
+ elif shape == "hp":
232
+ h = hp(f_signed, p["f_cutoff"], m)
233
+ else:
234
+ f_low, f_high = p["f_cutoff"]
235
+ f0 = 0.5 * (f_high + f_low)
236
+ df = 0.5 * (f_high - f_low)
237
+ if shape == "bp":
238
+ h = lp(f_signed - f0, df, m) + lp(f_signed + f0, df, m)
239
+ elif shape == "bs":
240
+ h = hp(f_signed - f0, df, m) * hp(f_signed + f0, df, m)
241
+ else:
242
+ raise ValueError(f"Unrecognized filter shape {shape!r}.")
243
+
244
+ b = numpy.roll(numpy.fft.ifft(h).real, (taps - 1) // 2)
245
+
246
+ if b.size < 1:
247
+ raise RuntimeError("FIR kernel design failed.")
248
+
249
+ self._b = b.real
250
+ self._a = numpy.empty(0)
251
+ self._x = numpy.empty(b.size - 1, dtype=float)
252
+ self._y = numpy.empty(0, dtype=float)
253
+
254
+ self._family = "digital"
255
+
256
+ elif self._family in ("rc", "butterworth", "bessel", "cheby1"):
257
+ shape = p["shape"]
258
+ if shape == "lp":
259
+ f_cutoff = p["f_cutoff"]
260
+ if not (0 < f_cutoff < f_nyquist):
261
+ raise ValueError(
262
+ f"'f_cutoff' ({f_cutoff}) must be positive and less than "
263
+ f"'1 / (2 * time_step)' ({f_nyquist})"
264
+ )
265
+ w0 = 0.0
266
+ wc = 2.0 * numpy.pi * f_cutoff
267
+ elif shape == "hp":
268
+ f_cutoff = p["f_cutoff"]
269
+ if not (0 < f_cutoff < f_nyquist):
270
+ raise ValueError(
271
+ f"'f_cutoff' ({f_cutoff}) must be positive and less than "
272
+ f"'1 / (2 * time_step)' ({f_nyquist})"
273
+ )
274
+ w0 = 0.0
275
+ wc = 2.0 * numpy.pi * f_cutoff
276
+ elif shape == "bp":
277
+ f_low, f_high = p["f_cutoff"]
278
+ if not (0 < f_low < f_high < f_nyquist):
279
+ raise ValueError(
280
+ f"'f_low' ({f_low}) and 'f_high' ({f_high}) must be positive and "
281
+ f"satisfy 'f_low < f_high < 1 / (2 * time_step)' ({f_nyquist})"
282
+ )
283
+ w0 = numpy.pi * (f_low + f_high)
284
+ wc = numpy.pi * (f_high - f_low)
285
+ elif shape == "bs":
286
+ f_low, f_high = p["f_cutoff"]
287
+ if not (0 < f_low < f_high < f_nyquist):
288
+ raise ValueError(
289
+ f"'f_low' ({f_low}) and 'f_high' ({f_high}) must be positive and "
290
+ f"satisfy 'f_low < f_high < 1 / (2 * time_step)' ({f_nyquist})"
291
+ )
292
+ w0 = numpy.pi * (f_low + f_high)
293
+ wc = numpy.pi * (f_high - f_low)
294
+ else:
295
+ raise ValueError(f"Unrecognized filter shape {shape!r}.")
296
+
297
+ if self._family == "rc":
298
+ if shape == "lp":
299
+ poles = [-wc]
300
+ residues = [wc, 0.0]
301
+ elif shape == "hp":
302
+ poles = [-wc]
303
+ residues = [-wc, 1.0]
304
+ elif shape == "bp":
305
+ poles = [-wc - 1j * w0]
306
+ residues = [wc, 0.0]
307
+ elif shape == "bs":
308
+ poles = [-wc - 1j * w0]
309
+ residues = [-wc, 1.0]
310
+
311
+ else:
312
+ btype = "lowpass" if shape in ("lp", "bp") else "highpass"
313
+ if self._family == "butterworth":
314
+ b, a = signal.butter(p["order"], wc, btype=btype, analog=True)
315
+ elif self._family == "bessel":
316
+ b, a = signal.bessel(p["order"], wc, btype=btype, analog=True, norm="mag")
317
+ else:
318
+ b, a = signal.cheby1(p["order"], p["ripple"], wc, btype=btype, analog=True)
319
+ residues, poles, k = signal.residue(b, a)
320
+ if btype == "lowpass":
321
+ k = numpy.zeros(1)
322
+ residues = numpy.concatenate((residues, k))
323
+ poles = poles - 1j * w0
324
+
325
+ self._time_domain_model = TimeDomainModel(
326
+ PoleResidueMatrix(poles, {tuple(self.keys[:2]): residues}),
327
+ time_step,
328
+ )
329
+
330
+ self._family = "time_domain_model"
331
+
332
+ else:
333
+ raise ValueError(f"Unrecognized filter family {self._family!r}.")
334
+
335
+ self.reset()
336
+
337
+ @property
338
+ def status(self):
339
+ if not self._setup_thread.is_alive() and self._status["message"] == "running":
340
+ self._status["message"] = "error"
341
+ with self._lock:
342
+ return self._status
343
+
344
+ def reset(self):
345
+ """Reset internal state."""
346
+ if self._family == "tunable_lp_rc":
347
+ self._sqrt_r_in = numpy.real(self._z0) ** 0.5
348
+ self._y = 0.0
349
+ elif self._family == "digital":
350
+ self._x.fill(0.0)
351
+ self._y.fill(0.0)
352
+ elif self._family == "time_domain_model":
353
+ self._time_domain_model.reset()
354
+
355
+ def step_single(
356
+ self,
357
+ inputs: numpy.ndarray,
358
+ outputs: numpy.ndarray,
359
+ time_index: int,
360
+ update_state: bool,
361
+ shutdown: bool,
362
+ ) -> None:
363
+ """Take a single time step on the given inputs.
364
+
365
+ Args:
366
+ inputs: Input values at the current time step. Must be a 1D array of
367
+ complex values ordered
368
+ according to :attr:`keys`.
369
+ outputs: Pre-allocated output array where results will be stored.
370
+ Same size and type as ``inputs``.
371
+ time_index: Time series index for the current input.
372
+ update_state: Whether to update the internal stepper state.
373
+ shutdown: Whether this is the last call to the single stepping
374
+ function for the provided :class:`TimeSeries`.
375
+ """
376
+ if self._family == "tunable_lp_rc":
377
+ v_ctrl = self._sqrt_r_in * inputs[2].real
378
+ f_cutoff = self._f_cutoff(v_ctrl)
379
+ alpha = min(1.0, max(0.0, numpy.exp(-2.0 * numpy.pi * f_cutoff * self._time_step)))
380
+ y = self._y * alpha + inputs[0].real * (1.0 - alpha)
381
+ outputs[1] = self._gain * y
382
+ if update_state:
383
+ self._y = y
384
+ elif self._family == "digital":
385
+ x = inputs[0].real
386
+ y = self._b[0] * x + numpy.dot(self._b[1:], self._x) - numpy.dot(self._a, self._y)
387
+ outputs[1] = self._gain * y
388
+ if update_state:
389
+ if self._x.size > 0:
390
+ self._x[1:] = self._x[:-1]
391
+ self._x[0] = x
392
+ if self._y.size > 0:
393
+ self._y[1:] = self._y[:-1]
394
+ self._y[0] = y
395
+ elif self._family == "time_domain_model":
396
+ self._time_domain_model.step_single(inputs, outputs, time_index, update_state, shutdown)
397
+ outputs[1] *= self._gain
398
+
399
+
400
+ register_time_stepper_class(FilterTimeStepper)