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,684 @@
|
|
|
1
|
+
import struct
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
import numpy
|
|
8
|
+
from scipy.optimize import minimize
|
|
9
|
+
|
|
10
|
+
from .. import typing as pft
|
|
11
|
+
from ..cache import _mode_overlap_cache, cache_s_matrix
|
|
12
|
+
from ..circuit_base import _process_component_netlist
|
|
13
|
+
from ..extension import (
|
|
14
|
+
Component,
|
|
15
|
+
Model,
|
|
16
|
+
Path,
|
|
17
|
+
Port,
|
|
18
|
+
SMatrix,
|
|
19
|
+
_connect_s_matrices,
|
|
20
|
+
config,
|
|
21
|
+
frequency_classification,
|
|
22
|
+
register_model_class,
|
|
23
|
+
)
|
|
24
|
+
from ..utils import _gather_status
|
|
25
|
+
from .analytic import WaveguideModel
|
|
26
|
+
from .tidy3d import Tidy3DModel, _align_and_overlap
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _CircuitModelRunner:
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
runners: dict[Any, Any],
|
|
33
|
+
frequencies: Sequence[float],
|
|
34
|
+
component_name: str,
|
|
35
|
+
ports: dict[str, Port],
|
|
36
|
+
port_connections: dict[str, tuple[int, str, int]],
|
|
37
|
+
connections: Sequence[tuple[tuple[int, str, int], tuple[int, str, int]]],
|
|
38
|
+
instance_port_data: Sequence[tuple[Any, Any]],
|
|
39
|
+
cost_estimation: bool,
|
|
40
|
+
) -> None:
|
|
41
|
+
self.runners = runners
|
|
42
|
+
self.frequencies = frequencies
|
|
43
|
+
self.component_name = component_name
|
|
44
|
+
self.ports = ports
|
|
45
|
+
self.port_connections = port_connections
|
|
46
|
+
self.connections = connections
|
|
47
|
+
self.instance_port_data = instance_port_data
|
|
48
|
+
self.cost_estimation = cost_estimation
|
|
49
|
+
|
|
50
|
+
self.lock = threading.Lock()
|
|
51
|
+
self._s_matrix = None
|
|
52
|
+
self._status = {"progress": 0, "message": "running", "tasks": {}}
|
|
53
|
+
|
|
54
|
+
self.thread = threading.Thread(daemon=True, target=self._run_and_monitor_task)
|
|
55
|
+
self.thread.start()
|
|
56
|
+
|
|
57
|
+
def _run_and_monitor_task(self):
|
|
58
|
+
task_status = _gather_status(*self.runners.values())
|
|
59
|
+
w_tasks = 3 * len(task_status["tasks"])
|
|
60
|
+
n_ports = len(self.instance_port_data)
|
|
61
|
+
n_connections = len(self.connections)
|
|
62
|
+
denominator = max(1, w_tasks + n_ports + n_connections)
|
|
63
|
+
|
|
64
|
+
with self.lock:
|
|
65
|
+
self._status = dict(task_status)
|
|
66
|
+
self._status["progress"] *= w_tasks / denominator
|
|
67
|
+
|
|
68
|
+
while task_status["message"] == "running":
|
|
69
|
+
time.sleep(0.3)
|
|
70
|
+
task_status = _gather_status(*self.runners.values())
|
|
71
|
+
with self.lock:
|
|
72
|
+
self._status = dict(task_status)
|
|
73
|
+
self._status["progress"] *= w_tasks / denominator
|
|
74
|
+
|
|
75
|
+
if task_status["message"] == "error":
|
|
76
|
+
with self.lock:
|
|
77
|
+
self._status = task_status
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
with self.lock:
|
|
81
|
+
self._status = task_status
|
|
82
|
+
if self.cost_estimation:
|
|
83
|
+
return
|
|
84
|
+
self._status["message"] = "running"
|
|
85
|
+
self._status["progress"] *= w_tasks / denominator
|
|
86
|
+
|
|
87
|
+
s_dict = {}
|
|
88
|
+
for index, (instance_ports, instance_keys) in enumerate(self.instance_port_data):
|
|
89
|
+
# Check if reference is needed
|
|
90
|
+
if instance_ports is None:
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
runner = self.runners[index]
|
|
94
|
+
s_matrix = runner if isinstance(runner, SMatrix) else runner.s_matrix
|
|
95
|
+
if s_matrix is None:
|
|
96
|
+
with self.lock:
|
|
97
|
+
self._status["message"] = "error"
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
# Fix port phases if a rotation is applied
|
|
101
|
+
mode_factor = {
|
|
102
|
+
f"{port_name}@{mode}": 1.0
|
|
103
|
+
for port_name, port in instance_ports
|
|
104
|
+
for mode in range(port.num_modes)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if instance_keys is not None:
|
|
108
|
+
for port_name, port in instance_ports:
|
|
109
|
+
key = instance_keys.get(port_name)
|
|
110
|
+
if key is None:
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
# Port mode
|
|
114
|
+
overlap = _mode_overlap_cache[key]
|
|
115
|
+
if overlap is None:
|
|
116
|
+
overlap = _align_and_overlap(
|
|
117
|
+
self.runners[(index, port_name, 0)].data,
|
|
118
|
+
self.runners[(index, port_name, 1)].data,
|
|
119
|
+
)[0]
|
|
120
|
+
_mode_overlap_cache[key] = overlap
|
|
121
|
+
|
|
122
|
+
for mode in range(port.num_modes):
|
|
123
|
+
mode_factor[f"{port_name}@{mode}"] = overlap[mode]
|
|
124
|
+
|
|
125
|
+
for (i, j), s_ji in s_matrix.elements.items():
|
|
126
|
+
s_dict[(index, i), (index, j)] = s_ji * mode_factor[i] / mode_factor[j]
|
|
127
|
+
|
|
128
|
+
with self.lock:
|
|
129
|
+
self._status["progress"] = 100 * (w_tasks + index + 1) / denominator
|
|
130
|
+
|
|
131
|
+
s_dict = _connect_s_matrices(s_dict, self.connections, len(self.instance_port_data))
|
|
132
|
+
|
|
133
|
+
# Build S matrix with desired ports
|
|
134
|
+
ports = {
|
|
135
|
+
(index, f"{ref_name}@{n}"): f"{port_name}@{n}"
|
|
136
|
+
for (index, ref_name, modes), port_name in self.port_connections.items()
|
|
137
|
+
for n in range(modes)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
elements = {
|
|
141
|
+
(ports[i], ports[j]): s_ji
|
|
142
|
+
for (i, j), s_ji in s_dict.items()
|
|
143
|
+
if i in ports and j in ports
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
with self.lock:
|
|
147
|
+
self._s_matrix = SMatrix(self.frequencies, elements, self.ports)
|
|
148
|
+
self._status["progress"] = 100
|
|
149
|
+
self._status["message"] = "success"
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def status(self) -> dict[str, Any]:
|
|
153
|
+
with self.lock:
|
|
154
|
+
return self._status
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def s_matrix(self) -> SMatrix:
|
|
158
|
+
with self.lock:
|
|
159
|
+
return self._s_matrix
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class CircuitModel(Model):
|
|
163
|
+
"""Model based on circuit-level S-parameter calculation.
|
|
164
|
+
|
|
165
|
+
The component is expected to be composed of interconnected references.
|
|
166
|
+
Scattering parameters are computed based on the S matrices from all
|
|
167
|
+
references and their interconnections.
|
|
168
|
+
|
|
169
|
+
The S matrix of each reference is calculated based on the active model
|
|
170
|
+
of the reference's component. Each calculation is preceded by an update
|
|
171
|
+
to the componoent's technology, the component itself, and its active
|
|
172
|
+
model by calling :attr:`Reference.update`. They are reset to their
|
|
173
|
+
original state after the :func:`CircuitModel.start` function is called.
|
|
174
|
+
Keyword arguents in :attr:`Reference.s_matrix_kwargs` will be passed on
|
|
175
|
+
to :func:`CircuitModel.start`.
|
|
176
|
+
|
|
177
|
+
If a reference includes repetitions, it is flattened so that each
|
|
178
|
+
instance is called separately.
|
|
179
|
+
|
|
180
|
+
Connections between incompatible ports (butt couplings) are handled
|
|
181
|
+
automatically. For electrical ports, if either one has impedance
|
|
182
|
+
information, the coupling matrix is approximated by reflection and
|
|
183
|
+
transmission coefficients derived from both port impedances. For optical
|
|
184
|
+
ports and electrical ports without impedance information, an
|
|
185
|
+
:class:`EMEModel` is used to calculate the butt coupling coefficients.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
mesh_refinement: Minimal number of mesh elements per wavelength used
|
|
189
|
+
for mode solving.
|
|
190
|
+
verbose: Flag setting the verbosity of mode solver runs.
|
|
191
|
+
|
|
192
|
+
See also:
|
|
193
|
+
- :func:`Component.get_netlist`
|
|
194
|
+
- :attr:`PortSpec.impedance`
|
|
195
|
+
- `Circuit Model guide <../guides/Circuit_Model.ipynb>`__
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
def __init__(
|
|
199
|
+
self,
|
|
200
|
+
mesh_refinement: pft.PositiveFloat | None = None,
|
|
201
|
+
verbose: bool = True,
|
|
202
|
+
):
|
|
203
|
+
super().__init__(mesh_refinement=mesh_refinement, verbose=verbose)
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def mesh_refinement(self):
|
|
207
|
+
return self.parametric_kwargs["mesh_refinement"]
|
|
208
|
+
|
|
209
|
+
@mesh_refinement.setter
|
|
210
|
+
def mesh_refinement(self, value):
|
|
211
|
+
self.parametric_kwargs["mesh_refinement"] = value
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def verbose(self):
|
|
215
|
+
return self.parametric_kwargs["verbose"]
|
|
216
|
+
|
|
217
|
+
@verbose.setter
|
|
218
|
+
def verbose(self, value):
|
|
219
|
+
self.parametric_kwargs["verbose"] = value
|
|
220
|
+
|
|
221
|
+
@cache_s_matrix
|
|
222
|
+
def start(
|
|
223
|
+
self,
|
|
224
|
+
component: Component,
|
|
225
|
+
frequencies: Sequence[float],
|
|
226
|
+
updates: dict[Sequence[str | int | None], dict[str, dict[str, Any]]] = {},
|
|
227
|
+
chain_technology_updates: bool = True,
|
|
228
|
+
verbose: bool | None = None,
|
|
229
|
+
cost_estimation: bool = False,
|
|
230
|
+
**kwargs: Any,
|
|
231
|
+
) -> _CircuitModelRunner:
|
|
232
|
+
"""Start computing the S matrix response from a component.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
component: Component from which to compute the S matrix.
|
|
236
|
+
frequencies: Sequence of frequencies at which to perform the
|
|
237
|
+
computation.
|
|
238
|
+
updates: Dictionary of parameter updates to be applied to
|
|
239
|
+
components, technologies, and models for references within the
|
|
240
|
+
main component. See below for further information.
|
|
241
|
+
chain_technology_updates: if set, a technology update will trigger
|
|
242
|
+
an update for all components using that technology.
|
|
243
|
+
verbose: If set, overrides the model's ``verbose`` attribute and
|
|
244
|
+
is passed to reference models.
|
|
245
|
+
cost_estimation: If set, Tidy3D simulations are uploaded, but not
|
|
246
|
+
executed. S matrix will *not* be computed.
|
|
247
|
+
**kwargs: Keyword arguments passed to reference models.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Result object with attributes ``status`` and ``s_matrix``.
|
|
251
|
+
|
|
252
|
+
The ``'updates'`` dictionary contains keyword arguments for the
|
|
253
|
+
:func:`Reference.update` function for the references in the component
|
|
254
|
+
dependency tree, such that, when the S parameter of a specific reference
|
|
255
|
+
are computed, that reference can be updated without affecting others
|
|
256
|
+
using the same component.
|
|
257
|
+
|
|
258
|
+
Each key in the dictionary is used as a reference specification. It must
|
|
259
|
+
be a tuple with any number of the following:
|
|
260
|
+
|
|
261
|
+
- ``name: str | re.Pattern``: selects any reference whose component name
|
|
262
|
+
matches the given regex.
|
|
263
|
+
|
|
264
|
+
- ``i: int``, directly following ``name``: limits the selection to
|
|
265
|
+
``reference[i]`` from the list of references matching the name. A
|
|
266
|
+
negative value will match all list items. Note that each repetiton in
|
|
267
|
+
a reference array counts as a single element in the list.
|
|
268
|
+
|
|
269
|
+
- ``None``: matches any reference at any depth.
|
|
270
|
+
|
|
271
|
+
Examples:
|
|
272
|
+
>>> updates = {
|
|
273
|
+
... # Apply component updates to the first "ARM" reference in
|
|
274
|
+
... # the main component
|
|
275
|
+
... ("ARM", 0): {"component_updates": {"radius": 10}}
|
|
276
|
+
... # Apply model updates to the second "BEND" reference under
|
|
277
|
+
... # any "SUB" references in the main component
|
|
278
|
+
... ("SUB", "BEND", 1): {"model_updates": {"verbose": False}}
|
|
279
|
+
... # Apply technology updates to references with component name
|
|
280
|
+
... # starting with "COMP_" prefix, at any subcomponent depth
|
|
281
|
+
... (None, "COMP.*"): {"technology_updates": {"thickness": 0.3}}
|
|
282
|
+
... }
|
|
283
|
+
>>> s_matrix = component.s_matrix(
|
|
284
|
+
... frequencies, model_kwargs={"updates": updates}
|
|
285
|
+
... )
|
|
286
|
+
|
|
287
|
+
See also:
|
|
288
|
+
- `Circuit Model guide <../guides/Circuit_Model.ipynb>`__
|
|
289
|
+
- `Cascaded Rings Filter example
|
|
290
|
+
<../examples/Cascaded_Rings_Filter.ipynb>`__
|
|
291
|
+
"""
|
|
292
|
+
if verbose is None:
|
|
293
|
+
verbose = self.verbose
|
|
294
|
+
s_matrix_kwargs = {}
|
|
295
|
+
else:
|
|
296
|
+
s_matrix_kwargs = {"verbose": verbose}
|
|
297
|
+
if cost_estimation:
|
|
298
|
+
s_matrix_kwargs["cost_estimation"] = cost_estimation
|
|
299
|
+
|
|
300
|
+
frequencies = numpy.array(frequencies, dtype=float, ndmin=1)
|
|
301
|
+
runners, _, component_ports, port_connections, connections, instance_port_data, _ = (
|
|
302
|
+
_process_component_netlist(
|
|
303
|
+
component,
|
|
304
|
+
frequencies,
|
|
305
|
+
self.mesh_refinement,
|
|
306
|
+
{},
|
|
307
|
+
updates,
|
|
308
|
+
chain_technology_updates,
|
|
309
|
+
verbose,
|
|
310
|
+
cost_estimation,
|
|
311
|
+
kwargs,
|
|
312
|
+
s_matrix_kwargs,
|
|
313
|
+
None,
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
return _CircuitModelRunner(
|
|
318
|
+
runners,
|
|
319
|
+
frequencies,
|
|
320
|
+
component.name,
|
|
321
|
+
component_ports,
|
|
322
|
+
port_connections,
|
|
323
|
+
connections,
|
|
324
|
+
instance_port_data,
|
|
325
|
+
cost_estimation,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Deprecated: kept for backwards compatibility with old phf files
|
|
329
|
+
@classmethod
|
|
330
|
+
def from_bytes(cls, byte_repr: bytes) -> "CircuitModel":
|
|
331
|
+
"""De-serialize this model."""
|
|
332
|
+
(version, verbose, mesh_refinement) = struct.unpack("<B?d", byte_repr)
|
|
333
|
+
if version != 0:
|
|
334
|
+
raise RuntimeError("Unsuported CircuitModel version.")
|
|
335
|
+
|
|
336
|
+
if mesh_refinement <= 0:
|
|
337
|
+
mesh_refinement = None
|
|
338
|
+
return cls(mesh_refinement, verbose)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _subpath_length(path, u0, u1, npts):
|
|
342
|
+
vecs = numpy.diff([path.at(u, output="position") for u in numpy.linspace(u0, u1, npts)], axis=0)
|
|
343
|
+
return ((vecs[:, 0] ** 2 + vecs[:, 1] ** 2) ** 0.5).sum()
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
_coupler_model = Tidy3DModel()
|
|
347
|
+
_arms_model = Tidy3DModel()
|
|
348
|
+
_circuit_model = CircuitModel()
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class DirectionalCouplerCircuitModel(Model):
|
|
352
|
+
"""Model for directional couplers based on circuit decomposition.
|
|
353
|
+
|
|
354
|
+
The coupler is subdivided into 5 parts: the 4 arms and the coupling
|
|
355
|
+
region. Each is simulated separately, and the final S parameters are
|
|
356
|
+
computed by a :class:`CircuitModel`.
|
|
357
|
+
|
|
358
|
+
The component geometry is expected to be composed of at least 2 paths
|
|
359
|
+
connecting 2 ports each. The paths must start far apart, come close
|
|
360
|
+
together in the coupling region, and separate again. The distance
|
|
361
|
+
between paths is used to determine the extents of the arms and coupling
|
|
362
|
+
region.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
coupling_region_model: Model used to compute the S parameters in the
|
|
366
|
+
coupling region.
|
|
367
|
+
arms_model: Model used for the arms. A dictionary mapping port names
|
|
368
|
+
to models can be used to set a specific models for each arm.
|
|
369
|
+
circuit_model: Model used to merge all component parts.
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
def __init__(
|
|
373
|
+
self,
|
|
374
|
+
*,
|
|
375
|
+
coupling_region_model: Model = _coupler_model,
|
|
376
|
+
arms_model: Model | dict[str, Model] = _arms_model,
|
|
377
|
+
circuit_model: Model = _circuit_model,
|
|
378
|
+
):
|
|
379
|
+
super().__init__(
|
|
380
|
+
coupling_region_model=coupling_region_model,
|
|
381
|
+
arms_model=arms_model,
|
|
382
|
+
circuit_model=circuit_model,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
def get_circuit(
|
|
386
|
+
self, component: Component, classification: Literal["optical", "electrical"] = "optical"
|
|
387
|
+
) -> Component:
|
|
388
|
+
"""Build the circuit equivalent for a component.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
component: Component to subdivide into arms and coupling region.
|
|
392
|
+
classification: Frequency classification to use.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
Subdivided component.
|
|
396
|
+
"""
|
|
397
|
+
ports = sorted(component.select_ports(classification).items(), key=lambda x: x[1].center[0])
|
|
398
|
+
if len(ports) != 4:
|
|
399
|
+
raise RuntimeError(
|
|
400
|
+
f"Component {component.name!r} must have 4 {classification} ports to use "
|
|
401
|
+
f"'DirectionalCouplerCircuitModel'."
|
|
402
|
+
)
|
|
403
|
+
if not all(isinstance(port, Port) for _, port in ports):
|
|
404
|
+
raise RuntimeError(f"All ports in {component.name!r} must be 'Port' instances.")
|
|
405
|
+
|
|
406
|
+
structures = component.structures
|
|
407
|
+
|
|
408
|
+
# Expect the following port configuration:
|
|
409
|
+
# P1 _ _ P3
|
|
410
|
+
# \_/
|
|
411
|
+
# P0 ----- P2
|
|
412
|
+
(n0, p0), (n1, p1), (n2, p2), (n3, p3) = ports
|
|
413
|
+
if p0.center[1] > p1.center[1]:
|
|
414
|
+
p0, p1 = p1, p0
|
|
415
|
+
n0, n1 = n1, n0
|
|
416
|
+
|
|
417
|
+
path_profiles = p0.spec.path_profiles_list()
|
|
418
|
+
if len(path_profiles) == 0:
|
|
419
|
+
raise RuntimeError(
|
|
420
|
+
"Path profiles must not be empty to use an 'DirectionalCouplerCircuitModel'."
|
|
421
|
+
)
|
|
422
|
+
layer0 = path_profiles[0][2]
|
|
423
|
+
|
|
424
|
+
# Find path0 that goes from p0 to p2
|
|
425
|
+
path0 = None
|
|
426
|
+
for s in structures[layer0]:
|
|
427
|
+
if not isinstance(s, Path):
|
|
428
|
+
continue
|
|
429
|
+
start = s.at(0, output="position")
|
|
430
|
+
end = s.at(s.size, output="position")
|
|
431
|
+
if numpy.allclose(p0.center, start):
|
|
432
|
+
if numpy.allclose(p2.center, end):
|
|
433
|
+
path0 = s
|
|
434
|
+
inverted0 = False
|
|
435
|
+
break
|
|
436
|
+
elif numpy.allclose(p1.center, end):
|
|
437
|
+
p1, p2 = p2, p1
|
|
438
|
+
n1, n2 = n2, n1
|
|
439
|
+
path0 = s
|
|
440
|
+
inverted0 = False
|
|
441
|
+
break
|
|
442
|
+
elif numpy.allclose(p3.center, end):
|
|
443
|
+
p3, p2 = p2, p3
|
|
444
|
+
n3, n2 = n2, n3
|
|
445
|
+
path0 = s
|
|
446
|
+
inverted0 = False
|
|
447
|
+
break
|
|
448
|
+
elif numpy.allclose(p0.center, end):
|
|
449
|
+
if numpy.allclose(p2.center, start):
|
|
450
|
+
path0 = s
|
|
451
|
+
inverted0 = True
|
|
452
|
+
break
|
|
453
|
+
elif numpy.allclose(p1.center, start):
|
|
454
|
+
p1, p2 = p2, p1
|
|
455
|
+
n1, n2 = n2, n1
|
|
456
|
+
path0 = s
|
|
457
|
+
inverted0 = True
|
|
458
|
+
break
|
|
459
|
+
elif numpy.allclose(p3.center, start):
|
|
460
|
+
p3, p2 = p2, p3
|
|
461
|
+
n3, n2 = n2, n3
|
|
462
|
+
path0 = s
|
|
463
|
+
inverted0 = True
|
|
464
|
+
break
|
|
465
|
+
if path0 is None:
|
|
466
|
+
raise RuntimeError(
|
|
467
|
+
f"Unable to find a path in layer {layer0} conneting port {n0!r} to any other port "
|
|
468
|
+
f"in component {component.name!r}."
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
v_axis = p2.center - p0.center
|
|
472
|
+
axis = 0 if abs(v_axis[0]) >= abs(v_axis[1]) else 1
|
|
473
|
+
if p1.center[axis] > p3.center[axis]:
|
|
474
|
+
p3, p1 = p1, p3
|
|
475
|
+
n3, n1 = n1, n3
|
|
476
|
+
|
|
477
|
+
# Find path1 from p3 to p1
|
|
478
|
+
path_profiles = p3.spec.path_profiles_list()
|
|
479
|
+
if len(path_profiles) == 0:
|
|
480
|
+
raise RuntimeError(
|
|
481
|
+
"Path profiles must not be empty to use an 'DirectionalCouplerCircuitModel'."
|
|
482
|
+
)
|
|
483
|
+
layer1 = path_profiles[0][2]
|
|
484
|
+
path1 = None
|
|
485
|
+
for s in structures[layer1]:
|
|
486
|
+
if not isinstance(s, Path):
|
|
487
|
+
continue
|
|
488
|
+
start = s.at(0, output="position")
|
|
489
|
+
end = s.at(s.size, output="position")
|
|
490
|
+
if numpy.allclose(p3.center, start) and numpy.allclose(p1.center, end):
|
|
491
|
+
path1 = s
|
|
492
|
+
inverted1 = False
|
|
493
|
+
break
|
|
494
|
+
elif numpy.allclose(p1.center, start) and numpy.allclose(p3.center, end):
|
|
495
|
+
path1 = s
|
|
496
|
+
inverted1 = True
|
|
497
|
+
break
|
|
498
|
+
if path1 is None:
|
|
499
|
+
raise RuntimeError(
|
|
500
|
+
f"Unable to find a path in layer {layer1} conneting ports {n1!r} and {n3!r} in "
|
|
501
|
+
f"component {component.name!r}."
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
offset0 = p0.spec.width * 2**-0.5
|
|
505
|
+
offset1 = p3.spec.width * 2**-0.5
|
|
506
|
+
|
|
507
|
+
if inverted0:
|
|
508
|
+
offset0 = -offset0
|
|
509
|
+
if inverted1:
|
|
510
|
+
offset1 = -offset1
|
|
511
|
+
|
|
512
|
+
v_axis = 0.5 * (p2.center - p0.center + p3.center - p1.center)
|
|
513
|
+
v_norm = 0.5 * (p1.center - p0.center + p3.center - p2.center)
|
|
514
|
+
if v_axis[0] * v_norm[1] - v_axis[1] * v_norm[0] < 0:
|
|
515
|
+
# if the normal is to the right of the propagation axis, invert the offsets
|
|
516
|
+
offset0 = -offset0
|
|
517
|
+
offset1 = -offset1
|
|
518
|
+
|
|
519
|
+
size0 = path0.size
|
|
520
|
+
npts0 = path0.spine().shape[0]
|
|
521
|
+
|
|
522
|
+
size1 = path1.size
|
|
523
|
+
npts1 = path1.spine().shape[0]
|
|
524
|
+
|
|
525
|
+
# Find crossings between path0 and path1 with respective offsets
|
|
526
|
+
def distance_sq(u):
|
|
527
|
+
x0, _, _, g0 = path0.at(u[0] * size0)
|
|
528
|
+
x1, _, _, g1 = path1.at(u[1] * size1)
|
|
529
|
+
n0 = numpy.array([-g0[1], g0[0]]) * (g0[0] ** 2 + g0[1] ** 2) ** -0.5
|
|
530
|
+
n1 = numpy.array([-g1[1], g1[0]]) * (g1[0] ** 2 + g1[1] ** 2) ** -0.5
|
|
531
|
+
x0 += n0 * offset0
|
|
532
|
+
x1 += n1 * offset1
|
|
533
|
+
v = x1 - x0
|
|
534
|
+
return v[0] ** 2 + v[1] ** 2
|
|
535
|
+
|
|
536
|
+
minimize_kwargs = {"method": "Nelder-Mead", "options": {"adaptive": False, "maxiter": 500}}
|
|
537
|
+
|
|
538
|
+
bounds = [(0.5, 1.0) if inverted0 else (0.0, 0.5), (0.0, 0.5) if inverted1 else (0.5, 1.0)]
|
|
539
|
+
x0 = [bounds[0][0] + 0.25, bounds[1][0] + 0.25]
|
|
540
|
+
res = minimize(distance_sq, x0=x0, bounds=bounds, **minimize_kwargs)
|
|
541
|
+
if not res.success and res.fun > (2 * config.tolerance) ** 2:
|
|
542
|
+
raise RuntimeError(
|
|
543
|
+
f"Unable to find start of coupling region from path intersections in component "
|
|
544
|
+
f"{component.name!r}."
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
u = res.x[0] * size0
|
|
548
|
+
center0, _, _, grad = path0.at(u)
|
|
549
|
+
angle0 = numpy.arctan2(grad[1], grad[0]) / numpy.pi * 180.0
|
|
550
|
+
length0 = _subpath_length(path0, u, size0 if inverted0 else 0.0, npts0)
|
|
551
|
+
|
|
552
|
+
u = res.x[1] * size1
|
|
553
|
+
center1, _, _, grad = path1.at(u)
|
|
554
|
+
angle1 = numpy.arctan2(grad[1], grad[0]) / numpy.pi * 180.0
|
|
555
|
+
length1 = _subpath_length(path1, u, 0.0 if inverted1 else size1, npts1)
|
|
556
|
+
|
|
557
|
+
bounds = [(0.0, 0.5) if inverted0 else (0.5, 1.0), (0.5, 1.0) if inverted1 else (0.0, 0.5)]
|
|
558
|
+
# x0 = [1.0 - res.x[0], 1.0 - res.x[1]]
|
|
559
|
+
x0 = [bounds[0][0] + 0.25, bounds[1][0] + 0.25]
|
|
560
|
+
res = minimize(distance_sq, x0=x0, bounds=bounds, **minimize_kwargs)
|
|
561
|
+
if not res.success and res.fun > (2 * config.tolerance) ** 2:
|
|
562
|
+
raise RuntimeError(
|
|
563
|
+
f"Unable to find start of coupling region from path intersections in component "
|
|
564
|
+
f"{component.name!r}."
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
u = res.x[0] * size0
|
|
568
|
+
center2, _, _, grad = path0.at(u)
|
|
569
|
+
angle2 = numpy.arctan2(grad[1], grad[0]) / numpy.pi * 180.0
|
|
570
|
+
length2 = _subpath_length(path0, u, 0.0 if inverted0 else size0, npts0)
|
|
571
|
+
|
|
572
|
+
u = res.x[1] * size1
|
|
573
|
+
center3, _, _, grad = path1.at(u)
|
|
574
|
+
angle3 = numpy.arctan2(grad[1], grad[0]) / numpy.pi * 180.0
|
|
575
|
+
length3 = _subpath_length(path1, u, size1 if inverted1 else 0.0, npts1)
|
|
576
|
+
|
|
577
|
+
if inverted0:
|
|
578
|
+
angle0 += 180
|
|
579
|
+
else:
|
|
580
|
+
angle2 += 180
|
|
581
|
+
|
|
582
|
+
if inverted1:
|
|
583
|
+
angle3 += 180
|
|
584
|
+
else:
|
|
585
|
+
angle1 += 180
|
|
586
|
+
|
|
587
|
+
p = self.parametric_kwargs
|
|
588
|
+
arms_model = p["arms_model"]
|
|
589
|
+
if isinstance(arms_model, Model):
|
|
590
|
+
arms_model = {n0: arms_model, n1: arms_model, n2: arms_model, n3: arms_model}
|
|
591
|
+
|
|
592
|
+
coupler = Component(f"Coupling region - {component.name}", technology=component.technology)
|
|
593
|
+
offset = max(abs(offset0), abs(offset1))
|
|
594
|
+
arms = []
|
|
595
|
+
for i, (name, port, center, angle, length) in enumerate(
|
|
596
|
+
[
|
|
597
|
+
(n0, p0, center0, angle0, length0),
|
|
598
|
+
(n1, p1, center1, angle1, length1),
|
|
599
|
+
(n2, p2, center2, angle2, length2),
|
|
600
|
+
(n3, p3, center3, angle3, length3),
|
|
601
|
+
]
|
|
602
|
+
):
|
|
603
|
+
coupler.add_port(Port(center, angle, port.spec, extended=False), name)
|
|
604
|
+
|
|
605
|
+
c = Component(f"Arm {i} - {component.name}", technology=component.technology)
|
|
606
|
+
c.add_port(port, name)
|
|
607
|
+
c.add_port(Port(center, angle - 180, port.spec.inverted(), extended=False), f"X{i}")
|
|
608
|
+
|
|
609
|
+
# Get bounds from ports only; we don't want the original geometry bounds
|
|
610
|
+
a, b = c.bounds()
|
|
611
|
+
bounds = [a - offset, b + offset]
|
|
612
|
+
|
|
613
|
+
arm_model = arms_model.get(name, _arms_model)
|
|
614
|
+
if (
|
|
615
|
+
isinstance(arm_model, WaveguideModel)
|
|
616
|
+
and arm_model.parametric_kwargs["length"] is None
|
|
617
|
+
):
|
|
618
|
+
arm_model = arm_model.__copy__().update(length=length)
|
|
619
|
+
elif isinstance(arm_model, Tidy3DModel):
|
|
620
|
+
b = arm_model.parametric_kwargs["bounds"]
|
|
621
|
+
if all(b[i][j] is None for i in (0, 1) for j in (0, 1)):
|
|
622
|
+
arm_model = arm_model.__copy__().update(
|
|
623
|
+
bounds=[(*bounds[0], b[0][2]), (*bounds[1], b[1][2])]
|
|
624
|
+
)
|
|
625
|
+
c.add_model(arm_model)
|
|
626
|
+
|
|
627
|
+
c.add(component)
|
|
628
|
+
arms.append(c)
|
|
629
|
+
|
|
630
|
+
# Get bounds from ports only; we don't want the original geometry bounds
|
|
631
|
+
a, b = coupler.bounds()
|
|
632
|
+
bounds = [a - offset, b + offset]
|
|
633
|
+
|
|
634
|
+
# If the new ports are too close to the original ones, we must make sure to extend them for
|
|
635
|
+
# a correct simulation
|
|
636
|
+
port_extension = -component.size().max()
|
|
637
|
+
coupler.add(component)
|
|
638
|
+
for port in (p0, p1, p2, p3):
|
|
639
|
+
endpoint = port_extension * numpy.exp(1j * port.input_direction / 180.0 * numpy.pi)
|
|
640
|
+
for layer, path in port.spec.get_paths(port.center):
|
|
641
|
+
coupler.add(layer, path.segment(endpoint, relative=True))
|
|
642
|
+
|
|
643
|
+
coupler_model = p["coupling_region_model"]
|
|
644
|
+
if isinstance(coupler_model, Tidy3DModel):
|
|
645
|
+
b = coupler_model.parametric_kwargs["bounds"]
|
|
646
|
+
if all(b[i][j] is None for i in (0, 1) for j in (0, 1)):
|
|
647
|
+
coupler_model = coupler_model.__copy__().update(
|
|
648
|
+
bounds=[(*bounds[0], b[0][2]), (*bounds[1], b[1][2])]
|
|
649
|
+
)
|
|
650
|
+
coupler.add_model(coupler_model)
|
|
651
|
+
|
|
652
|
+
circuit = Component(f"Circuit - {component.name}", technology=component.technology)
|
|
653
|
+
circuit.add(*arms, coupler)
|
|
654
|
+
circuit.add_virtual_connection_by_instance(0, "X0", 4, n0)
|
|
655
|
+
circuit.add_virtual_connection_by_instance(1, "X1", 4, n1)
|
|
656
|
+
circuit.add_virtual_connection_by_instance(2, "X2", 4, n2)
|
|
657
|
+
circuit.add_virtual_connection_by_instance(3, "X3", 4, n3)
|
|
658
|
+
circuit.add_port(p0, n0)
|
|
659
|
+
circuit.add_port(p1, n1)
|
|
660
|
+
circuit.add_port(p2, n2)
|
|
661
|
+
circuit.add_port(p3, n3)
|
|
662
|
+
circuit.add_model(p["circuit_model"])
|
|
663
|
+
|
|
664
|
+
return circuit
|
|
665
|
+
|
|
666
|
+
@cache_s_matrix
|
|
667
|
+
def start(self, component: Component, frequencies: Sequence[float], **kwargs: Any):
|
|
668
|
+
"""Start computing the S matrix response from a component.
|
|
669
|
+
|
|
670
|
+
Args:
|
|
671
|
+
component: Component from which to compute the S matrix.
|
|
672
|
+
frequencies: Sequence of frequencies at which to perform the
|
|
673
|
+
computation.
|
|
674
|
+
**kwargs: Keyword arguments passed to sub-models.
|
|
675
|
+
|
|
676
|
+
Returns:
|
|
677
|
+
Result object with attributes ``status`` and ``s_matrix``.
|
|
678
|
+
"""
|
|
679
|
+
circuit = self.get_circuit(component, frequency_classification(frequencies))
|
|
680
|
+
return circuit.active_model.start(circuit, frequencies, **kwargs)
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
register_model_class(CircuitModel)
|
|
684
|
+
register_model_class(DirectionalCouplerCircuitModel)
|
|
@@ -6,10 +6,9 @@ from typing import Any, Literal
|
|
|
6
6
|
|
|
7
7
|
import numpy
|
|
8
8
|
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
from
|
|
12
|
-
from .extension import (
|
|
9
|
+
from .. import typing as pft
|
|
10
|
+
from ..cache import cache_s_matrix
|
|
11
|
+
from ..extension import (
|
|
13
12
|
Component,
|
|
14
13
|
Interpolator,
|
|
15
14
|
Model,
|
|
@@ -25,6 +24,7 @@ from .extension import (
|
|
|
25
24
|
pole_residue_fit,
|
|
26
25
|
register_model_class,
|
|
27
26
|
)
|
|
27
|
+
from .analytic import _add_bb_text, _bb_layer
|
|
28
28
|
|
|
29
29
|
InterpolationMethod = Literal["linear", "barycentric", "cubicspline", "pchip", "akima", "makima"]
|
|
30
30
|
InterpolationCoords = Literal["real_imag", "mag_phase"]
|