Skip to content

Commit 21fb5ea

Browse files
flaportclaude
andauthored
Fix portless sub-netlists and internal-port handling (#97)
* Fix portless sub-netlists and internal-port handling (#95, #96) - Filter out sub-netlists with no ports in circuit() to prevent errors during construction of hierarchical netlists. - Change default behavior for ports on internal connection nodes: drop them instead of auto-converting to probes. Add on_internal_port flag to circuit() with modes "warn" (default), "ignore", and "as_probes". - Update and add tests for both fixes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * update docs * fix notebooks --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6f5d930 commit 21fb5ea

7 files changed

Lines changed: 371 additions & 82 deletions

File tree

nbs/examples/14_probes.ipynb

Lines changed: 27 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,9 @@
4646
"import sax"
4747
]
4848
},
49-
{
50-
"cell_type": "code",
51-
"execution_count": null,
52-
"id": "4",
53-
"metadata": {},
54-
"outputs": [],
55-
"source": [
56-
"sax.__file__"
57-
]
58-
},
5949
{
6050
"cell_type": "markdown",
61-
"id": "5",
51+
"id": "4",
6252
"metadata": {},
6353
"source": [
6454
"## Define Component Models\n",
@@ -69,7 +59,7 @@
6959
{
7060
"cell_type": "code",
7161
"execution_count": null,
72-
"id": "6",
62+
"id": "5",
7363
"metadata": {},
7464
"outputs": [],
7565
"source": [
@@ -101,7 +91,7 @@
10191
},
10292
{
10393
"cell_type": "markdown",
104-
"id": "7",
94+
"id": "6",
10595
"metadata": {},
10696
"source": [
10797
"## MZI Circuit\n",
@@ -121,7 +111,7 @@
121111
{
122112
"cell_type": "code",
123113
"execution_count": null,
124-
"id": "8",
114+
"id": "7",
125115
"metadata": {},
126116
"outputs": [],
127117
"source": [
@@ -154,7 +144,7 @@
154144
},
155145
{
156146
"cell_type": "markdown",
157-
"id": "9",
147+
"id": "8",
158148
"metadata": {},
159149
"source": [
160150
"## Adding Probes\n",
@@ -167,7 +157,7 @@
167157
{
168158
"cell_type": "code",
169159
"execution_count": null,
170-
"id": "10",
160+
"id": "9",
171161
"metadata": {},
172162
"outputs": [],
173163
"source": [
@@ -183,7 +173,7 @@
183173
},
184174
{
185175
"cell_type": "markdown",
186-
"id": "11",
176+
"id": "10",
187177
"metadata": {},
188178
"source": [
189179
"The circuit now has additional ports for each probe. Each probe creates two ports:\n",
@@ -194,7 +184,7 @@
194184
{
195185
"cell_type": "code",
196186
"execution_count": null,
197-
"id": "12",
187+
"id": "11",
198188
"metadata": {},
199189
"outputs": [],
200190
"source": [
@@ -205,7 +195,7 @@
205195
},
206196
{
207197
"cell_type": "markdown",
208-
"id": "13",
198+
"id": "12",
209199
"metadata": {},
210200
"source": [
211201
"## Simulating with Probes\n",
@@ -216,7 +206,7 @@
216206
{
217207
"cell_type": "code",
218208
"execution_count": null,
219-
"id": "14",
209+
"id": "13",
220210
"metadata": {},
221211
"outputs": [],
222212
"source": [
@@ -231,7 +221,7 @@
231221
},
232222
{
233223
"cell_type": "markdown",
234-
"id": "15",
224+
"id": "14",
235225
"metadata": {},
236226
"source": [
237227
"### Output Transmission\n",
@@ -242,7 +232,7 @@
242232
{
243233
"cell_type": "code",
244234
"execution_count": null,
245-
"id": "16",
235+
"id": "15",
246236
"metadata": {},
247237
"outputs": [],
248238
"source": [
@@ -260,7 +250,7 @@
260250
},
261251
{
262252
"cell_type": "markdown",
263-
"id": "17",
253+
"id": "16",
264254
"metadata": {},
265255
"source": [
266256
"### Signal at Probe Points\n",
@@ -271,7 +261,7 @@
271261
{
272262
"cell_type": "code",
273263
"execution_count": null,
274-
"id": "18",
264+
"id": "17",
275265
"metadata": {},
276266
"outputs": [],
277267
"source": [
@@ -289,15 +279,15 @@
289279
},
290280
{
291281
"cell_type": "markdown",
292-
"id": "19",
282+
"id": "18",
293283
"metadata": {},
294284
"source": [
295285
"As expected with a 50/50 coupler, the signal is split equally between the two arms.\n"
296286
]
297287
},
298288
{
299289
"cell_type": "markdown",
300-
"id": "20",
290+
"id": "19",
301291
"metadata": {},
302292
"source": [
303293
"## Probes Don't Affect Circuit Behavior\n",
@@ -308,7 +298,7 @@
308298
{
309299
"cell_type": "code",
310300
"execution_count": null,
311-
"id": "21",
301+
"id": "20",
312302
"metadata": {},
313303
"outputs": [],
314304
"source": [
@@ -324,15 +314,15 @@
324314
},
325315
{
326316
"cell_type": "markdown",
327-
"id": "22",
317+
"id": "21",
328318
"metadata": {},
329319
"source": [
330320
"The outputs are identical (within numerical precision), confirming that probes are purely observational."
331321
]
332322
},
333323
{
334324
"cell_type": "markdown",
335-
"id": "23",
325+
"id": "22",
336326
"metadata": {},
337327
"source": [
338328
"## Summary\n",
@@ -354,18 +344,18 @@
354344
},
355345
{
356346
"cell_type": "markdown",
357-
"id": "24",
347+
"id": "23",
358348
"metadata": {},
359349
"source": [
360350
"## Ports as probes\n",
361351
"\n",
362-
"When a port is applied on an internal node (i.e., an instance port that is already part of a connection), it will automatically be interpreted as a probe. SAX will issue a warning and create `{name}_fwd` and `{name}_bwd` ports instead of the original port name."
352+
"When a port is applied on an internal node (i.e., an instance port that is already part of a connection) it usually is ignored, however we can also make it it automatically be interpreted as a probe:"
363353
]
364354
},
365355
{
366356
"cell_type": "code",
367357
"execution_count": null,
368-
"id": "25",
358+
"id": "24",
369359
"metadata": {},
370360
"outputs": [],
371361
"source": [
@@ -397,7 +387,7 @@
397387
" \"waveguide\": waveguide,\n",
398388
"}\n",
399389
"\n",
400-
"circuit, _ = sax.circuit(mzi_netlist, models)\n",
390+
"circuit, _ = sax.circuit(mzi_netlist, models, on_internal_port=\"as_probes\")\n",
401391
"\n",
402392
"S = circuit(\n",
403393
" wl=wl,\n",
@@ -411,7 +401,7 @@
411401
{
412402
"cell_type": "code",
413403
"execution_count": null,
414-
"id": "26",
404+
"id": "25",
415405
"metadata": {},
416406
"outputs": [],
417407
"source": [
@@ -432,7 +422,7 @@
432422
},
433423
{
434424
"cell_type": "markdown",
435-
"id": "27",
425+
"id": "26",
436426
"metadata": {},
437427
"source": [
438428
"## Hierarchical Probes\n",
@@ -445,7 +435,7 @@
445435
{
446436
"cell_type": "code",
447437
"execution_count": null,
448-
"id": "28",
438+
"id": "27",
449439
"metadata": {},
450440
"outputs": [],
451441
"source": [
@@ -500,7 +490,7 @@
500490
{
501491
"cell_type": "code",
502492
"execution_count": null,
503-
"id": "29",
493+
"id": "28",
504494
"metadata": {},
505495
"outputs": [],
506496
"source": [

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ dev = [
1414
"scikit-learn>=1.6.1",
1515
"tmm>=0.2.0",
1616
"towncrier>=24.0.0",
17-
"ty>=0.0.1a11"
17+
"ty>=0.0.20"
1818
]
1919
docs = [
2020
"altair>=5.5.0",

src/sax/circuits.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@
2828
__all__ = ["circuit", "draw_dag", "get_required_circuit_models"]
2929

3030

31+
def _filter_portless_subnets(
32+
netlist: sax.RecursiveNetlist,
33+
) -> sax.RecursiveNetlist:
34+
"""Remove sub-netlists that have no ports (except the top-level entry)."""
35+
top_level_name = next(iter(netlist))
36+
return {
37+
name: flatnet
38+
for name, flatnet in netlist.items()
39+
if name == top_level_name or flatnet.get("ports", {})
40+
}
41+
42+
3143
@overload
3244
def circuit(
3345
netlist: dict[str, Any],
@@ -37,6 +49,7 @@ def circuit(
3749
top_level_name: str = "top_level",
3850
ignore_impossible_connections: bool = False,
3951
probes: dict[str, str] | None = None,
52+
on_internal_port: Literal["warn", "ignore", "as_probes"] = "warn",
4053
) -> tuple[sax.SDictModel, sax.CircuitInfo]: ...
4154

4255

@@ -50,6 +63,7 @@ def circuit(
5063
top_level_name: str = "top_level",
5164
ignore_impossible_connections: bool = False,
5265
probes: dict[str, str] | None = None,
66+
on_internal_port: Literal["warn", "ignore", "as_probes"] = "warn",
5367
) -> tuple[sax.SDictModel, sax.CircuitInfo]: ...
5468

5569

@@ -63,6 +77,7 @@ def circuit(
6377
top_level_name: str = "top_level",
6478
ignore_impossible_connections: bool = False,
6579
probes: dict[str, str] | None = None,
80+
on_internal_port: Literal["warn", "ignore", "as_probes"] = "warn",
6681
) -> tuple[sax.SDenseModel, sax.CircuitInfo]: ...
6782

6883

@@ -76,6 +91,7 @@ def circuit(
7691
top_level_name: str = "top_level",
7792
ignore_impossible_connections: bool = False,
7893
probes: dict[str, str] | None = None,
94+
on_internal_port: Literal["warn", "ignore", "as_probes"] = "warn",
7995
) -> tuple[sax.SCooModel, sax.CircuitInfo]: ...
8096

8197

@@ -88,6 +104,7 @@ def circuit(
88104
top_level_name: str = "top_level",
89105
ignore_impossible_connections: bool = False,
90106
probes: dict[str, str] | None = None,
107+
on_internal_port: Literal["warn", "ignore", "as_probes"] = "warn",
91108
) -> tuple[sax.Model, sax.CircuitInfo]:
92109
"""Create a circuit function for a given netlist.
93110
@@ -113,6 +130,10 @@ def circuit(
113130
connection and exposes forward and backward traveling wave ports.
114131
For a probe named "X" at instance port "inst,port", two new circuit
115132
ports are created: "X_fwd" and "X_bwd". Defaults to None.
133+
on_internal_port: How to handle top-level ports that map to internal
134+
connection nodes. ``"warn"`` (default) drops them with a warning,
135+
``"ignore"`` drops them silently, ``"as_probes"`` converts them
136+
to measurement probes (legacy behaviour).
116137
117138
Returns:
118139
Tuple containing:
@@ -158,13 +179,14 @@ def waveguide(length=10.0, neff=2.4, wl=1.55):
158179
)
159180
patch_netlist_array_instances(recnet)
160181
recnet = sax.into[sax.RecursiveNetlist](recnet)
161-
recnet, auto_probes = extract_port_probes(recnet)
182+
recnet, auto_probes = extract_port_probes(recnet, on_internal_port)
162183
if auto_probes:
163184
probes = {**(probes or {}), **auto_probes}
164185
if probes:
165186
recnet = expand_probes(recnet, probes)
166187
models = {"_ideal_probe": ideal_probe, **(models or {})}
167188
recnet = resolve_array_instances(recnet)
189+
recnet = _filter_portless_subnets(recnet)
168190
recnet = remove_unused_instances(recnet)
169191
_validate_netlist_ports(recnet)
170192
dependency_dag = _create_dag(recnet, models, validate=True)

src/sax/interpolation.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,10 @@ def to_df(
116116
obj: an xarray or a sax.SType object.
117117
target_name: the name of the target column
118118
in the dataframe (ignored when obj is an SType).
119-
kwargs: the coordinates of the SType values axes
120-
(ignored when obj is an xarray).
119+
kwargs: the coordinates of the SType values axes. You probably want to supply
120+
'wl' or 'f' here, i.e. the coordinates you supplied to generate the SType.
121+
The kwargs are ignored when the object is an xarray as the coordinates are
122+
already embedded in the xarray in that case.
121123
"""
122124
if isinstance(obj, xr.DataArray):
123125
xarr = obj

0 commit comments

Comments
 (0)