Skip to content

Releases

v0.5.0 — downloadable binaries, the cscript arity contract, and on crates.io

Choose a tag to compare

@Elvis-codeur Elvis-codeur released this 09 Nov 09:20

The release to use. Same code as v0.4.0, which was tagged minutes earlier — the version number moved to 0.5.0 so that the download and the crates.io packages agree, and crates.io never reuses a version. Until now the only way to get this simulator was to build it, which meant a Rust toolchain and three sibling checkouts first.

Download

Platform File
Windows x86-64 general-simulator-windows-x86_64.zip
Linux x86-64 general-simulator-linux-x86_64.tar.gz
Linux arm64 general-simulator-linux-aarch64.tar.gz
macOS Apple silicon general-simulator-macos-aarch64.tar.gz

Unpack and run it — there is nothing to install. On Windows:

> general-simulator.exe circuit.cir --mode transient --tfinal 1e-3 --dt 1e-6

Every binary was made to solve a resistive divider and checked to give 0.5 before it was packaged.

What is already inside it

Waveform measurements (kind=measure — RMS, THD, peak, TRIG-TARG, Fourier, error norms over non-uniform samples) are compiled in. There is no separate measurement program to install.

C blocks (kind=cscript) work too: the binary loads your .so/.dll at run time, so nothing was needed at build time.

What is not

kind=pyblock and kind=pyfunc are absent from these builds, deliberately. Embedding Python links the binary against one exact libpython, so a download built here would refuse to start against the Python you have. (abi3, the usual fix, applies to extension modules and not to an embedding binary — measured: the linked soname is identical with and without it.) Build locally with --features python for those two block kinds.

kind=octfunc / kind=octblock do work from the download: Octave is spawned as a subprocess rather than linked, so it only has to be installed on your machine, not on the build machine.

Since v0.3.0

A library can now declare its own arity. in_len/out_len used to be whatever the netlist said, and a .c file written for a different number indexed past the end of a buffer — a wrong answer at best, memory corruption at worst, and never a message naming the block. An optional int cscript_arity(int *in_len, int *out_len) is now checked against the netlist before the first step, and a disagreement is refused with both counts in the error. Absent, or returning non-zero, keeps the old behaviour exactly, so every existing .c file is unaffected.

Checkpoint and resume. --checkpoint-out writes a run's complete state at --tfinal, and --resume continues it bit-identically to a run that never stopped. cscript and octblock have their own state contracts so a block's internals survive the round trip.

Initial conditions on signal-domain blocks (ic=, y0=), alongside the existing ic= on capacitors and inductors.

The documentation is now verified, and gated. 152 tests assert that every ## Example and ## Errors claim in the Component Reference was actually run against this CLI, and CI now runs them — before, the suite existed and nothing executed it.

The netlist grammar chapter documents number suffixes, which it never did. Most importantly that 1M is one milliohm, not one megohm — SPICE's rule, case-insensitive, and the most expensive thing in the grammar to get wrong.

Both books (user guide and developer guide), the PDFs and the API reference are published at the repository's GitHub Pages site.

Related

On crates.io

Every crate is published at this version, so the library side is usable too:

$ cargo install general-simulator-cli      # the CLI, from the registry
crate version
general-simulator-cli 0.5.0
dae-runtime, continuous-blocks, lcp-solver, pwl-devices 0.5.0
cscript-ffi, pyblock-ffi, octave-ffi 0.5.0
general-mna 0.3.0

0.5.0 rather than 0.4.1 because it is breaking: DaeError gains
CScriptArityMismatch and is not #[non_exhaustive], so an exhaustive match downstream
stops compiling.

v0.4.0 — downloadable binaries, the cscript arity contract, checkpoint and resume

Choose a tag to compare

@Elvis-codeur Elvis-codeur released this 09 Nov 09:13

The first release with downloadable binaries. Until now the only way to get this simulator was to build it, which meant a Rust toolchain and three sibling checkouts first.

Download

Platform File
Windows x86-64 general-simulator-windows-x86_64.zip
Linux x86-64 general-simulator-linux-x86_64.tar.gz
Linux arm64 general-simulator-linux-aarch64.tar.gz
macOS Apple silicon general-simulator-macos-aarch64.tar.gz

Unpack and run it — there is nothing to install. On Windows:

> general-simulator.exe circuit.cir --mode transient --tfinal 1e-3 --dt 1e-6

Every binary was made to solve a resistive divider and checked to give 0.5 before it was packaged.

What is already inside it

Waveform measurements (kind=measure — RMS, THD, peak, TRIG-TARG, Fourier, error norms over non-uniform samples) are compiled in. There is no separate measurement program to install.

C blocks (kind=cscript) work too: the binary loads your .so/.dll at run time, so nothing was needed at build time.

What is not

kind=pyblock and kind=pyfunc are absent from these builds, deliberately. Embedding Python links the binary against one exact libpython, so a download built here would refuse to start against the Python you have. (abi3, the usual fix, applies to extension modules and not to an embedding binary — measured: the linked soname is identical with and without it.) Build locally with --features python for those two block kinds.

kind=octfunc / kind=octblock do work from the download: Octave is spawned as a subprocess rather than linked, so it only has to be installed on your machine, not on the build machine.

Since v0.3.0

A library can now declare its own arity. in_len/out_len used to be whatever the netlist said, and a .c file written for a different number indexed past the end of a buffer — a wrong answer at best, memory corruption at worst, and never a message naming the block. An optional int cscript_arity(int *in_len, int *out_len) is now checked against the netlist before the first step, and a disagreement is refused with both counts in the error. Absent, or returning non-zero, keeps the old behaviour exactly, so every existing .c file is unaffected.

Checkpoint and resume. --checkpoint-out writes a run's complete state at --tfinal, and --resume continues it bit-identically to a run that never stopped. cscript and octblock have their own state contracts so a block's internals survive the round trip.

Initial conditions on signal-domain blocks (ic=, y0=), alongside the existing ic= on capacitors and inductors.

The documentation is now verified, and gated. 152 tests assert that every ## Example and ## Errors claim in the Component Reference was actually run against this CLI, and CI now runs them — before, the suite existed and nothing executed it.

The netlist grammar chapter documents number suffixes, which it never did. Most importantly that 1M is one milliohm, not one megohm — SPICE's rule, case-insensitive, and the most expensive thing in the grammar to get wrong.

Both books (user guide and developer guide), the PDFs and the API reference are published at the repository's GitHub Pages site.

Related

v0.3.0 — ic= initial conditions, --out-every, a real silent-wrong-answer fix

Choose a tag to compare

@Elvis-codeur Elvis-codeur released this 14 Sep 06:37

Third checkpoint release — 6 commits since v0.2.0, one real silent-wrong-answer bug closed and
two new capabilities. See README.md for the architecture rationale and
book/dev-guide/src/design-decisions.md/docs/journal/ for the reasoning behind each of these.

Highlights

  • Fixed a real silent-wrong-answer bug: kind=sig2phys converters have no terminals and
    stamp nothing into the MNA system — they're named values consumed by reference (a source's
    own value field, a switch's gate=/ctrl=), never a circuit node. Because block names and
    circuit node names are separate namespaces, wiring a converter's name onto an element line
    (R1 VDRV 0 1k) built a perfectly ordinary undriven node — KCL solved it fine to 0, so the
    run silently reported V(VDRV) = 0 right next to the block's own correct, non-zero output
    column, in both --mode dc and --mode transient. DaeError::Sig2PhysUsedAsCircuitNode
    (reject_sig2phys_wired_into_circuit) now catches this before any system is built, in both
    modes, naming the converter, the offending element, and the node token as written.
  • ic= initial conditions are now honored end to end: general-mna v0.1.0 gained real
    ic= support on C/L (see its own release notes); this release is the seam that actually
    uses it — resolve_initial_state centralizes the decision across all four transient entry
    points (simulate_transient, simulate_transient_with_ideal_switches, the closed-loop runner,
    the streamed block-graph runner), so a run given no explicit x_initial now starts from
    whatever ic= the netlist declares instead of always from rest, falling back to rest only when
    the netlist declares none. An explicit x_initial from the caller still always wins.
  • --out-every N decimates CSV/raw output without coarsening the timestep: every step is
    still taken and the solution is bit-identical, only 1-in-N resolved points are written — for a
    case like verifying zero-voltage switching, where the integration step has to resolve a sub-
    picosecond hard-discharge time but a multi-microsecond commutation window doesn't need every
    point written to disk. kind=measure still sees every point regardless of --out-every (only
    the file, not the measurement, is thinned — decimating a measurement's own input would change
    its answer, not just its size). Tested by asserting kept rows are byte-for-byte the full run's
    rows at those indices, not just that the output shrank.

Fixed since v0.2.0: the two v0.2.0-era doc pages (resolve_initial_state's own doc comment,
DaeError::InitialCondition) that described the old solved-operating-point ic= semantics —
now backwards, since general-mna moved to a true assignment — were corrected alongside the
seam that consumes it.

v0.2.0 — signal-domain enforcement, scripting blocks, measurements, streaming output

Choose a tag to compare

@Elvis-codeur Elvis-codeur released this 04 Sep 07:30

Second checkpoint release — 47 commits since v0.1.0. Educational Rust simulator for
piecewise-linear circuits and mixed circuit/block-diagram systems continues to grow its
signal-domain block vocabulary and output tooling, while fixing a real memory-exhaustion crash
found and closed out this same cycle. See README.md for the architecture rationale and
book/dev-guide/src/design-decisions.md for the reasoning behind each of these.

Highlights

  • Enforced physical/signal-domain converters, the breaking change v0.1.0 was tagged
    immediately before: kind=sig2phys domain=voltage|current (unified from separate
    sig2voltage/sig2current kinds) is the only legal way a signal-domain block drives a
    source's magnitude or an ideal switch's gate; kind=phys2sig node=<n>|branch=<n> (renamed
    from probe) is the only way a circuit quantity re-enters the signal domain — a real,
    netlist-level physical/signal boundary, not just a naming convention.
  • Two new scripting block families: kind=pyfunc/kind=pyblock (embedded Python via
    PyO3, gated behind an optional python feature) and kind=octfunc/kind=octblock
    (GNU Octave via subprocess — deliberately never linked, since Octave is GPLv3 and this
    project is MIT — "mere aggregation" only, empirically validated for performance: a
    persistent session vs. a fresh process per call is ~4500x faster).
  • kind=cscript gained a real state contract: optional continuous state (xc), an
    optional update()/next_sample_hit() split for block-controlled discrete sample times, a
    to= phase offset — the same contract pyblock/octblock share.
  • Vector-signal support (SignalValue::Vector) — kind=statespace is now genuinely MIMO,
    not scalar-only.
  • New block kinds: discrete-time state-space/transfer-function/PID, logic gates, SR
    latches, flip-flops, a counter, and signal-domain SIN/PULSE/EXP/SFFM sources.
  • kind=measure: ngspice/Xyce-style post-processing measurements (MAX/MIN/RMS/
    FOUR/TRIG-TARG/ERROR/...) over a completed transient trace, backed by the
    newly-extracted gs-waveform-measurements crate (now its own sibling repo, independent
    release cadence). Results write to a <netlist>.log file, matching real SPICE tools' own
    .measure convention — not stdout, not stderr.
  • --format raw: writes a genuine binary SPICE rawfile, cross-validated against two real
    Python readers (PySpice, spicelib) reading a file this CLI actually wrote.
  • Adaptive stepping now lands exactly on discrete/PWM edges instead of silently missing
    them once dt_max approaches a switching period (earliest_next_event_dt) — closes a real
    bug demonstrated on a genuine MATLAB-ported phase-shift-PWM modulator (0/100 → 100/100 edges
    correctly resolved at the previously-failing step sizes).
  • Streaming CSV/raw output: the transient engine no longer buffers an entire run in memory
    before printing/writing anything — fixes a real incident where a large, stiff circuit under
    adaptive stepping drove a development machine to ~11GB RSS/24GB swap before being killed; the
    same run now holds ~23MB RSS regardless of length. A new --max-steps flag caps runaway
    adaptive runs as a hard safety net.
  • Generated Component Reference: 36+ kind=/BlockKind components fully documented, each
    backed by a committed doc-verify/ proof folder (a real .cir fixture + a Python test that
    actually runs the CLI and checks the documented claim).
  • Renamed Mosfet/Diode PWL models to IdealSwitch/IdealDiode; pyblock-ffi/pyo3 gated
    behind an optional python Cargo feature for a python-free build to stay possible.

Breaking changes since v0.1.0: kind=sig2voltage/kind=sig2current/kind=probe/
kind=sig2gate/kind=mosfet/kind=diode are all gone, no compatibility aliases — see the
items above for their replacements.

v0.1.0 — first checkpoint

Choose a tag to compare

@Elvis-codeur Elvis-codeur released this 22 Aug 20:34

First checkpoint release. Educational Rust simulator for piecewise-linear circuits and mixed circuit/block-diagram systems — no Newton-Raphson, no voltage limiting anywhere. See README.md for the full architecture rationale and docs/journal/2026-08.md for the development history.

This tag is a stable point immediately before a breaking netlist-grammar change: enforced physical/signal-domain converter blocks at every circuit-control boundary (tracked in book/dev-guide/src/open-questions.md).

Highlights

  • LCP-resolved PWL diode/MOSFET-body-diode segments (Lemke's algorithm), backward-Euler/trapezoidal DAE integration with automatic fallback on mode changes and trapezoidal ringing.
  • Full continuous-block library: PID (fixed and dynamic anti-windup bounds), state-space, transfer function, VCO, hysteresis, Clarke/Park coordinate transforms + PLL, PMSM (RK4).
  • Block-graph execution order derived automatically from the Signal::Block dependency graph, with genuine algebraic-loop detection (exact closing path reported).
  • Complementary half-bridge PWM switching (gate=dutyctrl/dutyctrlcomplement).
  • elspice-pwl-cli: single-file netlist-plus-controller convention, fixed and adaptive time stepping.