Conductance-Oscillator Co-Simulation Fidelity¶
This page records the measured Python-versus-Verilog co-simulation fidelity of the conductance-based edge-crossing oscillators enrolled in the all-models co-simulation harness, together with the architectural findings that emerged while enrolling them. It exists so the fidelity claims are documented per model and per layer rather than folded into a single headline number: the layers do not achieve uniform bit-exactness, and the honest statement is that each model reaches the fidelity its conditioning and the hardware transcendental resolution allow.
The three co-simulation layers are:
- hand — the maintained Python reference class
(
HodgkinHuxleyNeuron,MorrisLecarNeuron,ConnorStevensNeuron,WangBuzsakiNeuron). - schema — the schema-driven
UniversalNeuronrunner (EquationNeuron) built fromneurons/model_schemas/<model>.toml. - Q16.16 RTL — the SystemVerilog datapath emitted by
compiler.verilog_compiler, executed under Icarus Verilog (iverilog -g2012+vvp) with a 32-bit word and 16 fraction bits.
Per-model, per-layer fidelity¶
Fidelity is stated at the spike-count level (number of rising-edge threshold
crossings over a fixed driven protocol), which is the harness contract for these
non-resetting oscillators. State-level bit-exactness is a stronger property that
only the polynomial and piecewise-linear oscillators achieve; the conductance
models diverge at the least-significant bits because the hand class evaluates
transcendentals through Python math while the schema runner uses numpy, and
the two travel through distinct Runge-Kutta drivers.
| Model | States | Integrator | hand == schema | schema == Q16.16 RTL |
|---|---|---|---|---|
morris_lecar |
2 | single RK4 step per call | exact spike count (7 @ I=100, 3000 steps) |
exact over the whole I in [90, 110] band |
hodgkin_huxley |
4 | 100-sub-step macro RK4 (substeps=100) |
exact spike count (5 @ I=20, 60 macro steps) |
exact over a bounded window (I=15, macro=20); ±1 beyond it |
connor_stevens |
6 | 100-sub-step macro RK4 (substeps=100) |
exact spike count (10 @ I=100, 60 macro steps) |
exact over a bounded window (I=100, macro=20); ±1 beyond it |
wang_buzsaki |
3 | 50-sub-step macro Gauss-Seidel (substeps=50) |
exact spike count (3 @ I=10, 20 macro steps) |
exact over a bounded window (I=10, macro=20); ±1 beyond it |
Morris-Lecar is well conditioned: it takes a single RK4 step per step() call,
its tanh/cosh gating maps onto the fixed-point look-up datapath without
losing a crossing, and the three layers agree on the spike count exactly across
the whole enrolled current band. Connor-Stevens is a stiff six-state model whose
schema runner reproduces the hand model's spike count exactly once the macro-step
mode is used, but whose Q16.16 datapath holds the exact count only over a bounded
window and drifts by a single spike beyond it. The drift is root-caused below and
is not closed by widening the datapath.
Finding 1 — the conductance hand models macro-step¶
The maintained conductance reference classes advance a fixed number of inner
integration sub-steps inside one public step() call and take one threshold
decision at the macro boundary:
HodgkinHuxleyNeuronandConnorStevensNeuron: 100 innerdt = 0.01sub-steps per 1 ms macro step (round(1.0 / dt)).WangBuzsakiNeuron: 50 innerdt = 0.01sub-steps per 0.5 ms macro step (int(0.5 / dt)).MorrisLecarNeuronis the exception: a single integration step per call.
A schema runner that took a single dt step per step() could not reproduce
the sub-stepping models' action-potential count — it over-counted roughly one
crossing per sub-step because every inner step was exposed to the threshold
comparator. This motivated the macro-step integration mode.
Finding 2 — macro-step integration mode ([integration] substeps)¶
The schema DSL now accepts substeps = N in the [integration] table. The
runner (EquationNeuron) advances N inner integration steps via
_integrate_once and then takes a single macro-boundary spike decision, matching
the hand models' structure. The Verilog emitter lowers the same semantics with a
sub-step counter (_ss_cnt), performing one integration step per clock and
gating both the spike output and the rising-edge history register to the macro
boundary (_macro_boundary).
substeps = 1 (the default) is byte-identical to the previous single-step
datapath, so every already-enrolled model is unchanged. The emitter lowering is
proven bit-exact against the runner on the polynomial FitzHugh-Nagumo oscillator
at substeps = 1, 2, and 4 (runner and Verilog both count 8 crossings) — a
model with no transcendental look-up table to confound the comparison. The
emitter path is guarded to the rising-edge / no-reset / non-pipelined case and
raises NotImplementedError otherwise.
Finding 3 — the Connor-Stevens residual is LUT-resolution-limited, not datapath-precision-limited¶
Widening the fixed-point word does not close the Connor-Stevens spike-count band: the emitted datapath produces the identical spike count at Q16.16, Q24.24, and Q32.32. The limiting factor is the resolution of the hardware transcendental look-up tables, not the number of fraction bits.
The emitter (compiler.verilog_expr_emitter) lowers exp, exprel, tanh,
and cosh through 256-entry look-up tables sampled over [-16, 16) with a step
of 0.125 (lut_min = -16.0, lut_step = 0.125); sqrt, log, and the
cube-root a-gate use their own ranges. For the stiff six-state Connor-Stevens
gating, that 0.125-resolution sampling — not the datapath precision — sets the
point at which a marginal crossing is gained or lost. Closing the band would
require finer transcendental tables, which is a separate emitter axis, not a
precision knob. This is documented so a future reader does not spend effort
adding fraction bits that cannot help.
Finding 4 — integration order matters, not only the macro-step¶
Reproducing a hand model's spike count requires matching its integration order, not merely its sub-step count:
- Simultaneous (Jacobi-style): every derivative is evaluated from the single
pre-step state, then all states advance together. The schema runner's
eulerandrk4modes are simultaneous. Connor-Stevens' hand class uses simultaneous RK4, so the schemark4 + substeps=100path matches it. - Gauss-Seidel (sequential): the gates are updated first from the old
membrane voltage, then the membrane current is computed from the new gate
values and the old voltage. The Hodgkin-Huxley default integrator
(
baseline_euler) and the Wang-Buzsaki integrator are Gauss-Seidel, so the schema's simultaneous integration does not reproduce them.
Consequences for the conductance enrolments:
hodgkin_huxleyadditionally exposes anintegrator="rk4"option (a simultaneousRK4Solver), which the schemark4 + substeps=100path matches exactly — this is the enrolled operating point (hand == schemaatI=20, 60 macro steps). The defaultbaseline_eulerpath is Gauss-Seidel and would need a sequential schema mode to reproduce; enrolling against the higher-fidelityrk4path is faithful (same equations, a better integrator) and avoids that.wang_buzsakihas no RK4 option, so it needs a sequential (Gauss-Seidel) schema integration mode to be enrolled faithfully. That mode is now delivered: theEquationNeuronrunner and the Verilog emitter both carry agauss_seidelmethod that advances the state variables in declaration order (h,nbeforev), so a later variable reads the already-committed earlier ones — matching the hand model's gates-then-voltage update. With state orderedh, n, vandsubsteps=50, the schema reproducesWangBuzsakiNeuronexactly (hand == schemaatI=10, 20 macro steps).
Both are stiff, so a bounded-window ±1 Q16.16 band comparable to Connor-Stevens
is expected: Hodgkin-Huxley confirms it (exact three-way at I=15, macro=20;
±1 beyond), and Wang-Buzsaki now confirms it as well — three-way exact at I=10,
macro=20 (its m_inf = alpha_m/(alpha_m+beta_m) runtime division and exprel gating
lower to a fixed-point divide plus a 256-entry look-up table, the same look-up-table-
and fixed-point-resolution limit, not a datapath-precision limit).
Provenance corrections landed in-line¶
While enrolling Connor-Stevens two pre-existing defects were found and fixed in the same change rather than deferred:
- Wrong DOI. The
connor_stevensschema cited10.1113/jphysiol.1971.sp009368, which resolves at Crossref to a 1971 endotoxin-shock paper. The correct reference is10.1113/jphysiol.1971.sp009366(Connor & Stevens 1971, Prediction of repetitive firing behaviour from voltage clamp data on an isolated neurone soma), Crossref-verified before landing. - TOML/JSON rate-form drift.
connor_stevens.jsoncarried the singulara*(V - V0) / (1 - exp(...))rate form (undefined0/0atV = V0) while the loaded.tomlused the stableexprelrewrite. The loader prefers.toml, so the JSON was dead but inconsistent; it was synchronised to theexprelform.
Both corrections reinforce a standing rule: verify every DOI at Crossref before it lands, and keep the paired TOML/JSON schema forms in sync.
Resolved debt (was out of scope for the conductance enrolments)¶
src/scpn_neurocore/ — a separate three-file SCPN bridge package
(datastream.py, bridge.py) that is not in the CI strict-mypy gate
(mypy --strict src/sc_neurocore/) — carried 31 pre-existing type-arg and
arg-type strict-mypy errors. These were fixed as their own unit (bare
np.ndarray annotations parametrised as np.ndarray[Any, Any]; two
untrusted-payload validators widened from str to object), so
mypy --strict src/scpn_neurocore/ is now clean. Recorded here for the trail.
Verification¶
The conductance co-simulation parity is exercised by:
PYTHONPATH=src python -m pytest tests/test_cosimulation.py -q -k "morris_lecar or connor_stevens or macrostep"
The macro-step emitter bit-exactness (FitzHugh-Nagumo, no look-up table) is
exercised by the TestMacroStepSubstepEmitter class in the same module. The
schema reference traces for both oscillators are validated by
tests/test_reference_traces.py (see
Reference Trace Harness).