Skip to content

import circulax monkeypatches sax.Netlist to drop nets, corrupting unrelated SAX simulations #32

Description

@pepijndevos

Summary

Importing circulax monkeypatches sax.saxtypes.netlist.Netlist at import time with a type that omits the nets field. This is a global, process-wide side effect: after import circulax, SAX's own recursive-netlist coercion silently drops nets from every sub-netlist, which corrupts unrelated SAX simulations running in the same process.

Where

circulax/netlist.py:

circulaxNetlist = Annotated[
    TypedDict(
        "Netlist",
        {
            "instances": Instances,
            "connections": NotRequired[Connections],
            "ports": Ports,
            "placements": NotRequired[Placements],
            "settings": NotRequired[Settings],
            # note: no "nets" field
        },
    ),
    bval(sax_netlist.val_netlist),
]

Netlist = circulaxNetlist

# Monkeypatch sax.Netlist to be circulaxNetlist so that all functions using sax.Netlist
sax_netlist.Netlist = circulaxNetlist  # <-- global side effect on sax

Because sax.saxtypes.netlist.Netlist is reassigned, sax.into[RecursiveNetlist]val_recnettry_into[Netlist] (which resolves Netlist from the patched module) coerces every sub-netlist against a type that has no nets, so nets is dropped. The top-level sax.Netlist alias is not repatched, so a flat into[Netlist] still keeps nets — that asymmetry makes it easy to miss.

Impact

gdsfactory's cell.get_netlist(recursive=True) expresses a composite cell's internal wiring as nets (a list of pairwise {p1, p2} links). Once circulax is imported anywhere in a worker process, a subsequent plain SAX simulation of a composite cell (e.g. ring_single) loses that internal wiring — the ring's loop instances become disconnected and get pruned by remove_unused_instances, so a resonant ring degrades to a flat/monotonic response.

In our app (GDSFactory+) SAX and circulax run in the same per-PDK worker and circulax is imported lazily, so the symptom is: run SAX (correct), run circulax once, switch back to SAX → wrong result for the rest of the worker's life.

Minimal reproduction

import sax
# build any recnet whose sub-netlist expresses connectivity via `nets`, e.g. a
# gdsfactory composite: recnet = {"top": {...}, "ring_single": {"instances": {...},
#                                          "nets": [{"p1": "a,o1", "p2": "b,o1"}, ...],
#                                          "ports": {...}}}

before = sax.into[sax.RecursiveNetlist]({"ring_single": sub})   # sub has 6 nets
# -> nets preserved

import circulax  # import alone, no simulation

after = sax.into[sax.RecursiveNetlist]({"ring_single": sub})    # same sub dict
# -> nets == [] (silently dropped)

sax.into[sax.Netlist](sub) keeps the nets in both cases; only the recursive path drops them (it resolves Netlist from the patched module).

Suggested fix

Prefer not to mutate sax.saxtypes.netlist.Netlist globally. Options, roughly in order of preference:

  1. Don't reassign sax_netlist.Netlist at all — keep circulaxNetlist internal to circulax and reference it explicitly where circulax needs the legacy SAX format.
  2. If the patch is needed, include the nets field in circulaxNetlist so coercion is lossless for SAX consumers.
  3. At minimum, scope the change so it doesn't alter global SAX behavior for code that only uses SAX.

For context, nets and connections are equivalent in SAX (both pairwise 1:1 joins; _nets_to_connections raises on multiply-connected ports either way), so dropping nets loses connectivity that connections could have carried.

Environment

  • circulax 0.2.1
  • sax 0.18.2

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions