Skip to content

Port CAC2026 community batch: Wrongm, SSTADEX, SPARX (+SAR ADC handoff)#1

Merged
Mauricio-xx merged 10 commits into
mainfrom
feat/community-projects-batch1
May 18, 2026
Merged

Port CAC2026 community batch: Wrongm, SSTADEX, SPARX (+SAR ADC handoff)#1
Mauricio-xx merged 10 commits into
mainfrom
feat/community-projects-batch1

Conversation

@Mauricio-xx

Copy link
Copy Markdown
Owner

Lands four CAC2026 community project ports into eda-agents on top of the existing analog / RTL infrastructure. The batch covers Wrongm (#18) Ron/gm methodology for inverter-based amplifiers, SSTADEX (#16) hierarchical analog design-space exploration, SPARX (#10) RF Six-Port Receiver layout reproduction, and the standalone-team handoff for SAR ADC (#13) at /home/montanares/personal_exp/sar-adc-pathsim-pdk-port/.

PDK coverage: IHP SG13G2 for all four ports; Wrongm and SSTADEX additionally cross-ported to GF180MCU through the new PdkConfig.finger_param hook and PDK-aware spec floors.

New infrastructure: HierarchicalDseRunner (sibling of AutoresearchRunner with the same backend dispatch and program.md / results.tsv persistence), RonGmLookup (Wrongm-flavoured wrapper around GmIdLookup), GdsfactoryRunner (subprocess wrapper around .venv-gdsfactory mirroring the GLayoutRunner pattern), three new skill bundles (analog.ron_gm_sizing, analog.hierarchical_dse, plus prompt fragments), a new gdsfactory pytest marker, and scripts/patch_iic_jku_ihp.py as the idempotent helper for the three /foss/pdks hardcodes in iic-jku/IHP branch IHP-TO (with the corresponding upstream PR opened separately).

Validation gates met before opening this PR: SSTADEX 1-stage OTA Pareto reproduced on three corners with delta_db <= 0.41 dB on IHP and <= 1.89 dB on GF180 (the wider GF180 envelope is LUT-bound and documented in the topology); Wrongm IBA hits spec on both PDKs at its respective bias points (Iq=2.5 uA at 1.2 V on IHP per paper Table II; Adc=16.88 dB / GBW=14.15 MHz / Iq=16.33 uA at 3.3 V on GF180 with PDK-rescaled spec floors); SPARX sparx60_top.gds reproduced byte-for-byte against upstream in 3.5 s and KLayout DRC clean in 11.87 s under the SPARX Makefile's relaxed mode.

Non-spice test gate against the rebased branch: 1487 passed, 1 pre-existing failure (test_gl_sim_runner::test_missing_sdf_fails[ihp_sg13g2], untouched by this PR), 48 skipped, 63 deselected. Ruff clean on the 33 branch-touched .py files (the 12 pre-existing main files with lint warnings are out of scope here). Two pre-existing collection-time import bugs in tests/test_mcp_server.py and tests/test_veriloga_pipeline.py also untouched. SPARX gated test passes against the real .venv-gdsfactory.

The companion docs are docs/sparx_rf_pdk_variants.md (three-repo dependency chain, native bring-up cheat sheet, GF180 RF gap acknowledgment) and the per-topology source modules under src/eda_agents/topologies/{iba_ihp.py, iba_gf180.py, sstadex/, rf/sparx_six_port.py}. SAR ADC #13 stays out of eda-agents until the separate-team port lands at the handoff folder.

…026)

Adds RonGmLookup (core/ron_gm_lookup.py) wrapping the existing GmIdLookup
to derive Ron = Vds/Id analytically at a designer-chosen on-state, then
divides by gm at the bias point to expose Ron/gm. No new external LUT
data is shipped: the same PSP103 .npz LUT that powers gm/ID supplies
the per-unit-width currents. PMOS sign convention and Vgs clip handled
internally so the public API takes positive magnitudes for both
polarities.

InverterBasedAmplifier (topologies/iba_ihp.py) instantiates a single
CMOS inverter open-loop with CL=667 fF matching Wrøngm's reference IBA
(Tsettle=250 ns, UGB=9.55 MHz, Iq=2.5 µA). End-to-end SPICE smoke
test on IHP SG13G2 lands at trip-point Vbias=0.55-0.60 V with
Adc~32 dB, GBW~22 MHz, PM~90°, Iq~2.5 µA (matches Wrøngm Table II).
forbidden_insight_patterns block Ron-blind autoresearch suggestions.

analog.ron_gm_sizing skill bundle (core/sizing/corners) documents
the two-phase settling model, the RonGmLookup API and the corner
analysis discipline; registered in skills/analog.py mirroring the
miller_ota bundle precedent.

Apache-2.0 attribution to Nithin P et al. (Code-a-Chip VLSI26 #18)
preserved in module docstrings.

Tests, autoresearch A/B validation, and GF180 cross-port to follow.
Three new modules:
- tests/test_ron_gm_lookup.py (15 tests): per-width point lookups,
  Ron-scales-1/W, gm-scales-W, Ron/gm-scales-1/W^2, size dict shape
  parity with GmIdLookup.size(), Ibias matching, gmid_max subthreshold
  guard, PMOS positive-magnitude API, deadzone monotonicity. Skips
  when EDA_AGENTS_IHP_LUT_DIR or ihp-gmid-kit not available.
- tests/test_iba_ihp_topology.py (12 tests): design-space invariants,
  default params in space, relevant_skills ordering, forbidden-pattern
  match/no-overmatch, sizing W-clip + m rounding, netlist contains
  lib + devices + AC + op, multiplier-into-W absorption (no m= on
  IHP subcircuit instances). One @pytest.mark.spice integration test
  runs the deck through ngspice and asserts Adc>=20dB, GBW>=5MHz,
  PM>=60deg, Iq<10uA at the trip point.
- tests/test_ron_gm_skill.py (5 tests): skill registration, listing
  by prefix, render with + without topology context, soft token cap.

All 32 tests pass. ruff clean.
Deterministic walkthrough of the methodology with real SPICE:
1. RonGmLookup picks NMOS + PMOS sizes at the characterisation
   bias (5 uA, Ron/gm=50 MΩ/S, L=3 um / L=0.5 um).
2. Width-scales to the design bias (2.5 uA, Wrøngm reference).
3. SPICE-sweeps Vbias to find the inverter trip point.
4. Runs open-loop AC at the trip point, reports Adc / GBW / PM / Iq.
5. Documents the open-loop-AC-vs-cap-feedback-settling caveat that
   keeps strict numerical reproduction of Wrøngm Table II out of
   scope for this commit.

No LLM / API key required -- this is the deterministic gate that
proves the integration mechanics (lookup, topology, skill, ngspice
deck) before any autoresearch A/B is layered on top. The skill
content can be printed with --show-skill; the autoresearch A/B (skill
ON vs OFF) is wired through topo.relevant_skills() and triggers
automatically when AutoresearchRunner picks this topology.
Mirrors the upstream SSTADEx user-facing surface (Library / Primitive /
Macromodel / Testbench / dfs) on top of eda-agents' GmIdLookup and a
new sympy-based MNA solver. No XSCHEM, no mosplot subprocess, no
external SymMNA dependency: the symbolic transfer function is built
directly from the primitive small-signal branch list and the testbench
elements, then evaluated on the LUT sweep via lambdify.

Modules:
- symbolic_mna.py    sympy MNA admittance-matrix builder for resistors,
                     capacitors, voltage sources, current sources, and
                     VCCS (gm sources). Solves once per testbench in
                     a fraction of a second on a 13x13 1-stage OTA.
- characterization.py 4-D PSP103 .npz reader returning the SSTADEx
                     primitive DataFrame columns (length, width, gm,
                     gds, gdsid, Ro, cgg, cgs, cgd, vgs, vds) with
                     PMOS sign handled internally.
- primitives.py      Library + Port + Primitive base + four primitives
                     (simplediffpair / simplecurrentmirror /
                     simplecurrentsource / simplecommonsource) with
                     a bind-on-get factory pattern.
- macromodel.py      Macromodel + NetlistInstance with shared_nodes,
                     propagated_conditions, derived_metrics. Emits
                     the flattened small-signal element list that
                     Testbench.eval consumes.
- testbench.py       Testbench + bench elements (VoltageSource /
                     CurrentSource / Resistor / Capacitor) + Test
                     record with the upstream out_def vocabulary.
- dfs.py             Hierarchical depth-first explorer with
                     Cartesian-product sweep, shared_node filter,
                     propagated_conditions, paretoset Pareto.

Dependencies added to pyproject.toml: sympy>=1.11, pandas>=2.0,
paretoset>=1.2.

Smoke-tested on a 1-stage OTA configured per notebook cells 26-46:
3 primitives + submacromodel, no XSCHEM. dfs produces 450 valid
sized configurations in 0.27s, max gain ~42 V/V at L=6.4 um, in line
with the IHP SG13G2 PSP103 LUT at I_amp=20 uA.
…xample

Wraps eda_agents.topologies.sstadex.dfs() with the AutoresearchRunner
persistence convention (program.md + results.tsv + pareto.csv).
Single-shot run() emits the deterministic Pareto frontier; the
optional run_greedy(budget) mode iterates with the litellm or cc_cli
backend over a user-supplied macromodel knob space.

- agents/hierarchical_dse_runner.py: HierarchicalDseRunner with a
  macromodel_builder injection point, configurable FoM callable,
  backend dispatch shared with AutoresearchRunner.
- skills/_bundles/hierarchical_dse/{methodology,api,limits}.md +
  registration as analog.hierarchical_dse. ~14k char prompt under
  the 20k budget. Documents when to choose hierarchical DSE over
  gm/ID sizing or autoresearch and the corner / small-signal limits
  to keep in mind before trusting a Pareto endpoint.
- examples/17_sstadex_pareto_ihp.py: reproduces the upstream notebook
  1-stage OTA Pareto on IHP SG13G2 and optionally cross-validates
  three Pareto corners through ngspice.
- tests/test_hierarchical_dse_*.py: 26 unit tests covering MNA
  stamps, Library + primitive build, Macromodel + propagated
  conditions, Testbench.eval, skill registration, and the runner.
  All pass; 0 regressions on the non-spice gate.

Validation
==========

End-to-end run on IHP SG13G2 at I_amp=20 uA produced 19 Pareto
points in 0.27 s, gain 16.8 V/V (24.5 dB) to 42.4 V/V (32.6 dB).
ngspice cross-validation on three Pareto corners (smallest area,
middle gain, highest gain) agreed within 0.41 dB on every point --
under the 5 % gate required by the approved plan.

  W_diff_um  L_diff_um  W_al_um  L_al_um  gain_sym_db  gain_spice_db  delta_db
  1.39       0.4        4.91     0.4      24.51        24.20          -0.31
  7.90       6.4        8.10     0.8      29.63        30.04          +0.41
  7.90       6.4        56.89    6.4      32.56        32.86          +0.30

Side fixes on the schema commit (bd808ee)
=========================================
- _primitive_to_columns stringifies sympy Symbol keys so pandas does
  not split a single logical column into ``W_diff`` + ``Symbol('W_
  diff')``. apply_propagated_conditions resolves both representations.
- _cartesian_product_dfs drops overlap columns before merging so
  ``Symbol`` column names don't trip the default suffix-rename path
  (Symbol.endswith is undefined).
- Test and Testbench dataclasses opt out of pytest collection
  (__test__ = False) -- their names start with ``Test`` but they are
  schema dataclasses, not test classes.
GF180 sibling of example 17. Reuses the SSTADEX schema and the
HierarchicalDseRunner unchanged; only swaps the GmIdLookup PDK to
gf180mcu and rescales the bias rail to Vdd=3.3, Vref=Vout=2.0, vs
sweep [0.7, 1.3].

ngspice cross-validation on three Pareto corners yields
delta_db = -0.55 / +0.84 / +1.89 dB. The high-gain corner sits on
the LUT's coarser-resolution tail (GF180 Vds step = 100 mV vs IHP
50 mV), which is the structural reason the IHP-side 0.5 dB envelope
does not carry over. Pareto ordering is preserved across all three
corners.

tests/test_hierarchical_dse_runner_gf180.py mirrors the IHP test:
gated on the GF180 NMOS LUT being reachable from
EDA_AGENTS_GMID_LUT_DIR or the XDG auto-download cache.
Adds an InverterBasedAmplifierGF180 sibling to iba_ihp. Three small
infra changes make the parent deck-emission code PDK-agnostic so the
sibling inherits the whole netlist generator unchanged:

* core/pdk.py: PdkConfig.finger_param (default "ng", "nf" on GF180).
  IHP subcircuits expose ng (gate fingers), GF180 subcircuits expose
  nf -- the topology now reads the right name through the config.
* topologies/iba_ihp.py: hoist SPEC_ADC_DB / SPEC_GBW_HZ / SPEC_PM_DEG
  / SPEC_IQ_UA / CL_F from module constants to class attributes so
  subclasses can override per PDK without touching the methods.
* topologies/iba_ihp.py: _devline now emits
  "{self.pdk.finger_param}={value}" instead of hard-coded "ng=".

The GF180 sibling narrows the design space to the 3.3 V rail
(W_n in [0.22, 10] um, W_p in [0.22, 20] um, Vbias in [1.0, 2.5] V)
and sets ngspice-tuned spec floors (Adc>=15 dB, GBW>=5 MHz, Iq<=20 uA;
the IHP IBA's 9.55 MHz / 5 uA Wrøngm target does not transfer to
GF180 because the slower nfet_03v3 / pfet_03v3 cost current at the
trip point).

examples/19_wrongm_iba_gf180.py validates the methodology end-to-end:
RonGmLookup -> width-scale -> Vbias trip-point sweep on the GF180
deck. At Ibias_design=2.5 uA per branch the trip point lands at
Vbias=1.60 V with Adc=16.88 dB, GBW=14.15 MHz, Iq=16.33 uA -- all
within the GF180 spec floors. Wrøngm's IHP-specific Vds_on=0.05 V
convention is overridden to 0.1 V because GF180's coarser Vds LUT
grid pins the smallest non-zero sampled point at 0.1 V.

tests/test_iba_gf180_topology.py mirrors the IHP suite, including the
ngspice spice integration test gated on PDK_ROOT containing
gf180mcuD/. test_iba_ihp_topology.py keeps passing after the
class-attribute refactor (90 tests + 1 PDK-conditional skip).
Mirrors the GLayoutRunner pattern: a thin runner driving a stdin /
stdout JSON contract against `_gdsfactory_driver.py`, shipped as
package data and resolved via importlib.resources so editable and
wheel installs both work. The driver accepts a `module:callable`
factory plus a params dict; the callable may return a
gdsfactory.Component, a GDS path, or None (driver picks the newest
.gds in the output dir).

Auto-detects the worktree's `src/` and prepends it to PYTHONPATH so
eda-agents modules stay importable inside the gdsfactory venv. Per-
call `env_extra` lets the SPARX wrapper override PDK_ROOT without
bleeding into other gdsfactory consumers.

Tests: 10 structural (mock subprocess) plus 1 gated test under a
new `gdsfactory` pytest marker that drives the real venv.

Part of Session 5 (SPARX #10 CAC2026 RF Six-Port vertical).
Opens the rf/ topology package and lands the SPARX (#10 CAC2026)
upstream-driver wrapper. The wrapper invokes
`scripts/six_port_gen.py` via runpy with a synthesised argv mirroring
the upstream Makefile's build-layout target; it does not reimplement
the generator. All heavy imports (gdsfactory, ihp) stay inside the
function body so the module remains importable from the main
eda-agents venv where those are not installed.

Companion `scripts/patch_iic_jku_ihp.py` applies three idempotent
patches to a local clone of iic-jku/IHP (branch IHP-TO): replace the
hardcoded `/foss/pdks` literals in `ihp/__init__.py`, `ihp/tech.py`,
and `ihp/cells/utils.py` with PDK_ROOT-driven paths. Required for
native (non-IIC-OSIC-TOOLS-container) use.

Reproduction gate: build sparx60_top.gds and pass KLayout DRC against
the IHP SG13G2 DRC deck (Session 5 close).
Walks the three-repo dependency chain (iic-jku/SG13G2_SPARX,
iic-jku/IHP branch IHP-TO, iic-jku/IHP-Open-PDK), the three hardcoded
paths that need patching for native use, and the submodules that
must be initialised on the PDK fork. Records the GF180 gap (no
high-fT primitives, no Schottky barrier diode, fT below SPARX's
design points) so the RF vertical stays IHP-only and the team does
not retry that path.

Also lists upstream contribution targets (one PR to drop the three
patches, mainline IHP-Open-PDK absorbing the JKU RF primitives) that
collapse the fork dependencies over time.
@Mauricio-xx Mauricio-xx merged commit 9e5dcb4 into main May 18, 2026
1 of 5 checks passed
@Mauricio-xx Mauricio-xx deleted the feat/community-projects-batch1 branch May 18, 2026 15:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant