Skip to content

t27o: emit the M1 cell constants from a SPICE deck -- rc, rlc, lad3 and lad5 bit-exact with linear.t27 (Closes #10766) - #10781

Merged
gHashTag merged 1 commit into
masterfrom
claude/analog-m2b-emit-cell-10766
Oct 19, 2026
Merged

gHashTag merged 1 commit into
masterfrom
claude/analog-m2b-emit-cell-10766

Conversation

@gHashTag

Copy link
Copy Markdown
Owner

Closes #10766

M2b of #10486 (M2a was #10509). specs/tools/t27o/spice_convert.t27 now reads a flat deck of R, C, L and one input source as the linear cell of specs/analog/linear.t27 and prints its constants as t27: tri t27o deck.cir --emit-cell writes CELL_N, CELL_D and CELL_NV, with no per-circuit text. linear.t27 is not changed.

What the converter does

  • One parser. The M2a front end is extended, not duplicated (python3 -I tools/dupe_scan.py: ok, no new duplicate body). It now also accepts a B voltage source whose expression is a function of time alone (numbers, time, u u2 uramp abs sgn sin cos exp sqrt, + - * / ^), ic= on C and L, and .options method= maxord= trtol=. Every deck the M2a reader accepted prints the same inventory as before (checked on the 17 decks of specs/analog/decks against the master build: the 4 SAR comparator decks are byte-identical; the 13 that differ were all refused before and are now read, or refused later for their own reason).
  • The input. A V source, or a time-only B source, from a node to ground 0: its plus node is the cell's input node and the source is the column of B (its waveform does not matter, the run supplies u). A B that reads a node (v(, i(, a node name, @, a comparison) or is a current source is refused.
  • The fold. MNA stamps of the R, C and L cards into [A | C/dt | B], eliminate and mn_of of linear.t27, and q_sm of linear.t27 for the rounding, all by use (explicit item imports; t27c test-report compiles), so a word cannot differ from the cell's by a rounding rule. Values are read as the f64 nearest to the decimal text (dec_f64: one multiplication or division of exact operands, a test shows it equals the literals 1.0e-9, 5.0e-9, 1.0e-5, 2.2e-6, ...).
  • One part is written again, on purpose. linear.t27's stamp hands a local array to a function that writes it (stamp_pair(&w, ...)); t27c gen-rust lowers that to &mut &(w), which rustc refuses, and the Rust of this spec is what tri runs. pair_add and src_add take and return the matrix instead. The tests hold the copy to the original: the four pinned circuits below fail if the stamp rule drifts (12 mutants of the converter, e.g. C stamped as C*dt, the b-node sign of the inductor row, ZS fixed at 1, dt from tstep, are each killed by a test).

The four circuits, bit for bit

CELL_D / CELL_NV of each deck equal linear.t27's pinned constants word for word. Checked three ways: the spec tests (embedded text of the decks without their * lines), the Rust that tri runs on the real files, and the real tri t27o <deck> --emit-cell binary (debug build, own target dir).

deck states D words NV words result
rc_fixed.cir 1 1 (NEG | 798915) 1 (798915) RC_D, RC_NV: identical
rlc_fixed.cir 3 9 3 RLC_D, RLC_NV: identical
lad3_fixed.cir 3 9 3 LAD3_D, LAD3_NV: identical
lad5_fixed.cir 5 25 5 LAD5_D, LAD5_NV: identical

Refusal and negative control

  • A MOS line is refused naming the line: line 4: M element not supported plus the card; a PDK device (XM1 ... sg13_lv_nmos) is not in the M1 cell with its line. Also refused, each with its line and a test: an I or E source, a second or floating or upside-down input, no input, m=, ic other than 0 (card or .ic), a C or L on the input node, a node called gnd, a value that is zero, negative or not exact in f64, no states or more than NMAX = 5, no .tran with all four numbers, two analyses, an integration that is not method=gear maxord=1, and a matrix with no solution (a floating pair). Real decks: lad10_fixed.cir is refused at its line 18, the sixth node (the M1 cell has 1 to 5 states), rc_adaptive.cir is refused for its .tran.
  • Negative controls: the RC deck with R1 = 1.1k gives NEG | 729444 / 729444 (1/23) instead of 798915 (1/21); the ladder with R2 doubled changes the words; .tran with tmax = 100n gives NEG | 1525201 (1/11), so dt really is the fourth number.

The ordering rule

The unknowns are the free nodes numbered 1, 2, ... in the order their names first appear in the R, C and L cards (names compared without case; 0 is ground, the input node is NODE_IN, gnd is refused rather than guessed), then one per inductor in card order, its current stored times ZS. ZS = sqrt(L1 / C1) with L1 the first inductor and C1 the first capacitor of the deck, 1 when either is missing (100 Ohm for the RLC, as pinned). dt is the .tran tmax. The four decks name their nodes 1, 2, ... in that order, so the rule reproduces linear.t27's order without a per-circuit case; a deck that lists the far end of the ladder first gets the permuted words (tested against the permuted pinned arrays), and the emitted block lists which node or inductor each state is.

Own-language line counts

Hand-written Rust: cli/tri/src/main.rs +6 -4 (the --emit-cell flag and the dispatch, emit_cell is t27c gen-rust output), cli/tri/src/fpga.rs -26 (normalize_fixture_paths, an unreferenced #[cfg(test)] fn: git grep -w finds only its definition and a prose line in a report, and cargo check --tests -p tri warned it was never used). Net -24; 6 added, 30 deleted. No other hand-written code. gen/rust/tools/t27o/spice_convert.rs is t27c gen-rust output (byte-identical on a second run).

Gates

  • t27c test-report specs/tools/t27o/spice_convert.t27: 39 tests, 39 pass, FAIL 0, vacuous passes 0 of 39 (22 M2a tests unchanged except the B-source reason text, 17 new). t27b test: 39/39.
  • t27c seal ... --save then --verify: all five hashes MATCH; .trinity/seals/t27o_T27oSpice.json committed (closure: linear.t27, rust_convert.t27; no home path, not blocked).
  • cargo check -p tri and cargo check --tests -p tri pass; docs/reports/t27b_expectations.json untouched (spice_convert stays pass).

Not done yet

🤖 Generated with Claude Code

…nd lad5 fold to linear.t27's pinned words, bit for bit (Closes #10766)

M2b of the analog-emulation epic #10486. spice_convert.t27 reads a flat deck of R, C, L and one
input source (a V source, or a B source that is a function of time alone) as the linear cell of
specs/analog/linear.t27 and prints CELL_N, CELL_D and CELL_NV as t27: `tri t27o deck.cir
--emit-cell`. The words come from the netlist alone; no per-circuit text.

- One parser: the M2a front end is extended (a time-only B voltage is accepted as an input, ic= on
  C and L, .options method= maxord= trtol=), not copied. Decks the M2a reader accepted print the
  same inventory.
- The fold: MNA stamps of the cards, linear.t27's eliminate and mn_of, and linear.t27's q_sm for the
  rounding, by `use`. The stamp is written again here (pair_add, src_add) because linear.t27's stamp
  hands a local array to a function that writes it and gen-rust lowers that to `&mut &(w)`; the
  tests hold the copy to the original.
- States: free nodes in the order their names first appear in the R, C and L cards, then the
  inductors in card order (current times ZS = sqrt(L1/C1)); dt is the .tran tmax; the deck must say
  method=gear maxord=1 (backward Euler), else it is refused.
- Refused with line and reason: a MOS or any other element letter, an X card, an I or E source, a B
  source that reads a node or is a current source, a second or floating input, m=, ic other than
  0, C or L on the input node, a gnd node, more than 5 states, no .tran with four numbers, a
  matrix with no solution.
- 39 tests, 0 vacuous: the four decks equal RC/RLC/LAD3/LAD5 _D and _NV word for word; one
  resistor changed gives other words; reordered cards give the permuted words; 12 mutants of the
  converter are each killed by a test.
- Hand Rust in cli/tri: +6 -4 in main.rs (the flag and the dispatch), -26 in fpga.rs (the
  unreferenced #[cfg(test)] fn normalize_fixture_paths): net -24.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-10-19 02:46:00 UTC

Summary

Status Count
Total Open PRs 50
PRs with Failing Checks 40
PRs with All Checks Green 10
READY 2
FAILING 40
PENDING 0
NO CHECKS YET 0

These columns do not partition: 2 + 40 + 0 + 0 = 42, and there are 50 open PRs. A PR is being counted twice or not at all.

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=fad12492937a != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

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.

analog M2b: emit the M1 cell constants from a SPICE deck -- t27o spice_convert folds MNA to the cell words, bit-exact with M1

2 participants