Skip to content

analog M5: waveforms as ngspice RAW files that uberWAVE opens -- raw_writer.t27 checked against ngspice and uberWAVE's parser (Closes #10909) - #10940

Merged
gHashTag merged 1 commit into
masterfrom
claude/analog-m5-raw-10909
Oct 19, 2026
Merged

gHashTag merged 1 commit into
masterfrom
claude/analog-m5-raw-10909

Conversation

@gHashTag

Copy link
Copy Markdown
Owner

Closes #10909. Part of #10486 (M5, first slice).

What this adds

  • specs/analog/raw_writer.t27 (module AnalogRawWriter): writes the ngspice RAW file, ASCII and binary, for a time-major f64 trace; raw_check is the reader that judges a file. 18 tests.
  • specs/analog/decks/raw_ref.cir: the deck whose write output (ASCII and binary, ngspice-47) is the reference the writer must equal. Both files are embedded in the spec.
  • tri raw <out.raw> [--binary] [--date ...]: writes the M1 RLC trace (circuit 2 of linear.t27) as such a file. Glue only; every rule is the spec (gen/rust/analog/raw_writer.rs is t27c gen-rust of it, byte-identical).
  • .trinity/seals/analog_AnalogRawWriter.json. No analog_AnalogSar* seal and no sar_* / mos_* spec is touched.

The format, read off ngspice-47 (FR-001)

Header lines: Title, Date, Command (optional), Plotname, Flags: real, No. Variables: N, No. Points: P, Variables:, N lines <TAB>index<TAB>name<TAB>type, then Values: or Binary:. ASCII point: index<TAB>v0, then <TAB>vj for each other variable (one per line), then an empty line, every value C's %.15e. Binary point: N little-endian f64.

Three things the reference taught that a manual would not have:

  1. The index has a single leading space at every width ( 9, 10, 100, 1000), not a padded field.
  2. Command: is a command that ngspice executes when it loads the file (misplaced Command: line, AnalogRawWriter: no such command available in ngspice). Files written here carry none. ngspice's own Command: ngspice-47, Build is the one text it accepts; the reference test still writes it, to match the bytes.
  3. ngspice -r pads the point count with spaces after a run (No. Points: 220 ); the reader accepts that.

%.15e needs 16 significant digits of the exact binary value, rounded to nearest, ties to even. f64 arithmetic cannot do that, so the digits come from the exact decimal expansion of the double (base-1e9 limbs, at most 767 digits). The f64 <-> bits pair is built from arithmetic too, because gen-rust prints @bitCast as written and rustc refuses it (filed: #10920).

Tests (t27c test-report specs/analog/raw_writer.t27)

  tests       18
  pass        18
  FAIL        0
  vacuous passes  0 of 18  (passed with 0 runtime asserts executed)
  • The writer's ASCII output for the 15-point, 4-variable reference trace equals ngspice's file byte for byte; so does the binary output (Date line is the same parameter, no masking). The 60 input doubles are read off ngspice's binary file.
  • %.15e equals Python 3's '%.15e' % x on 35 vectors: round numbers, 1e-11 (just below the decimal), 1e23, exact ties that round down, up and carry (3000000000000000.5, ...1.5, 3999999999999999.5), subnormals, the smallest normal, the largest double, a dozen random ones.
  • The reader accepts ngspice's own two files and reads the binary values back bit for bit.
  • Negative controls, each refused with its own reason: one point too many or too few in the header (SHORT / TRAIL, ASCII and binary), one variable too many or too few (VARS / MODE), a body with 3 or 5 values per point against 4 variables (SHAPE), a wrong index, a broken value, a missing flags line, a missing mode line, a binary file one byte short or long.
  • The RLC trace: its fold of every state word equals RLC_EXP_S, which linear.t27 pins, so it is that simulation; a trace with one stored constant's sign flipped is not. Both files pass raw_check and the binary decodes exactly.
  • Writer mutants, each must fail (t27b test on a copy, deleted afterwards): A no space before the index (6 tests fail), B ties round up instead of to even (1), C big-endian binary (2), D point count one too small (6), E truncation instead of rounding (2).

FR-002: ngspice loads the file, uberWAVE's parser reads it

The file comes from the merged cell, written by the committed command (debug build of tri from this branch):

$ tri raw rlc_t27.raw --date "Mon Oct 19 14:30:00  2026"
$ tri raw rlc_t27_bin.raw --binary --date "Mon Oct 19 14:30:00  2026"
$ ls -l: rlc_t27.raw 24450 bytes, rlc_t27_bin.raw 8302 bytes
$ tri raw again.raw --date "Mon Oct 19 14:30:00  2026"; cmp again.raw rlc_t27.raw   -> same bytes

ngspice-47 (load then print, both modes; load.cir is a 5-line .control deck, not committed):

Loading raw data file ("rlc_ascii.raw") ...
done.
Title:  m1 series rlc, backward euler on a fixed 5 ns grid
Name: tran1 (Transient Analysis)
    i(l1)  : current, real, 201 long
    time   : time, real, 201 long [default scale]
    v(1)   : voltage, real, 201 long
    v(2)   : voltage, real, 201 long
    v(in)  : voltage, real, 201 long
time[0] = 0.000000e+00      time[1] = 5.000000e-09     time[2] = 1.000000e-08
v(in)[1] = 1.000000e+00     v(2)[1] = 2.375305e-03     v(2)[2] = 7.001758e-03
i(l1)[2] = 9.252936e-04     length(time) = 2.010000e+02

The binary file prints the same numbers. The spec's own value for v(2) at point 1 is 0.002375304698944092, at point 100 1.0759003162384033 (the capacitor rings past 1 V, damping 0.5). Rewritten by ngspice (load, then write ... all as binary) and compared with a scratch script (not committed): from the binary file bit-exact, from the ASCII file at most 4.4e-16 relative, which is the 16 digits of %.15e.

uberWAVE wp2-parser (GPL-2.0, commit c75a89c8f; its five source files fetched through the GitLab API into a scratch directory, built there with clang++ -std=c++20 -O1 -Iinclude main.cpp src/parser.cpp, deleted afterwards, never vendored):

$ uwparser --no-color rlc_ascii.raw
[INFO] Parsed 201 value rows.
[INFO] Parse of rlc_ascii.raw completed with 1 plot(s).
Title: m1 series rlc, backward euler on a fixed 5 ns grid
Plotname: Transient Analysis   Flags: real   No. Variables: 5   No. Points: 201
  0 time time | 1 v(in) voltage | 2 v(1) voltage | 3 v(2) voltage | 4 i(l1) current
Values rows: 201
$ uwparser --no-color rlc_bin.raw
[INFO] parseBinaryDataBlock: read 201 rows x 5 vars.
[INFO] Parse of rlc_bin.raw completed with 1 plot(s).

A short scratch C++ driver over RawFileParser::getColumn printed every column with %.17g: names time v(in) v(1) v(2) i(l1), 201 points each; the binary file's 1005 values are bit-exact with ours, the ASCII file's within 4.4e-16 relative.

Negative control (header says 202 or 200 points, 201 present): uberWAVE refuses 202 (Parsed rows 201 does not equal expected 202, Parsing failed) but reads 200 without complaint; ngspice reports Error: unable to read point count for 202 (and still builds a 202-long plot) and Error: strange line in rawfile: for 200 (no plot is loaded). The spec's raw_check refuses both (SHORT, TRAIL). Two observations for Symbiotic EDA, from this run only: uwparser --preview N streams a binary file as text (Skipped non-numeric token) for ngspice's own binary file as well as ours, while the full parse reads both; and a declared count smaller than the data is accepted.

FR-003: hand lines (own-language budget)

git diff --numstat: cli/tri/src/main.rs +15 -0 (a mod pair, one Commands variant, one dispatch arm, cmd_raw: allocate four buffers, call raw_rlc_file, write the file), cli/dlc10/src/lib.rs +0 -23. Net hand lines -8. The 23 deleted lines are extract_rx with its doc comment and #[allow(dead_code)]: git grep -nw extract_rx matched only its definition, and the dlc10 tests still compile (cargo test -p dlc10 --no-run). gen/rust/analog/raw_writer.rs (1025 lines) is generated and force-added, like the other tracked gen files; it is t27c gen-rust output, cmp-identical on regeneration. I did not touch cmd_t27o.

Gates run

What is not covered

  • Complex-valued (AC) files, several plots in one file, header lines beyond the ones above. A negative zero is written as zero (arithmetic cannot see its sign); NaN and infinities are written as nan, inf, -inf (ASCII) or the canonical pattern (binary).
  • The RLC trace time axis is k * dt; decks/rlc_fixed.cir shifts its axis by 42.5 ns, so the two traces overlay after that shift. Nothing here compares our trace with ngspice's trace of the same circuit.
  • M4e's sampled VDP/VDN and decided bits as a trace (the same writer, a second raw_*_trace function) are the next slice.
  • Nothing about a board, an oracle, power or speed.

🤖 Generated with Claude Code

…writer.t27, checked against ngspice and uberWAVE's parser (Closes #10909)

specs/analog/raw_writer.t27 writes the ngspice RAW file (ASCII and binary) for a
time-major f64 trace, with the variable table and the Date line as parameters, and
raw_check reads a file back and refuses one whose header lies about its points or
variables. The layout was read off ngspice-47 itself: specs/analog/decks/raw_ref.cir
writes one transient both ways and both files are embedded in the spec, so the writer's
output must equal ngspice's bytes. The ASCII values are C's %.15e, built from the exact
decimal expansion of the double (not f64 arithmetic), checked against 35 printf vectors
with ties, carries and subnormals.

tri raw writes the M1 RLC trace (linear.t27 circuit 2, the fold equals the pinned
RLC_EXP_S) as such a file; ngspice loads it and prints the same samples, and uberWAVE's
wp2-parser reads names, 201 points and the values (binary bit for bit, ASCII to 4.4e-16
relative). Hand Rust in this commit: +15 lines in cli/tri/src/main.rs, -23 (a dead
function, extract_rx in cli/dlc10, unreferenced) in cli/dlc10/src/lib.rs.

gen-rust cannot lower @bitcast (#10920), so the f64 <-> bits pair is arithmetic.

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

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-10-19 07:37:07 UTC

Summary

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

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

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=88b8cb73ceda != 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).

@gHashTag
gHashTag enabled auto-merge (squash) October 19, 2026 07:38
@gHashTag
gHashTag merged commit 00ecf46 into master Oct 19, 2026
34 of 37 checks passed
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 M5: waveforms as ngspice RAW files that uberWAVE opens -- a .t27 writer checked against ngspice and uberWAVE

2 participants