Skip to content

CircuitSimulation

public partial class CircuitSimulation : System.IDisposable

The circuit simulation a part belongs to. It steps every component and solves the circuit matrix each tick. Circuit logic reaches it through Simulation, and stamps into it.

public CircuitSimulation();

Not documented yet.

public NativeArray<RowInfo_t> CircuitRowInfo { get; }

Not documented yet.

public NativeArray<long> NodeMesh { get; }

Not documented yet.

public NativeArray<int> NodeMeshRowSize { get; }

Not documented yet.

public NativeArray<long> Leads { get; }

Not documented yet.

public NativeArray<long> NativeNodeList { get; }

Not documented yet.

public double* RightSide { get; }

Direct pointer to the C++ right_side array. Set by Set_right_side during Analyze. Non-linear components use this to stamp current sources directly without P/Invoke overhead.

public DynamicSingle NullStamp { get; }

Not documented yet.

public double PaceScale { get; set; }

Not documented yet.

public bool HasTimeFollower { get; }

Not documented yet.

public double FollowerHeldSeconds { get; }

Not documented yet.

public int LoopAnalyzeCount { get; }

Not documented yet.

public double LastLoopAnalyzeSimTime { get; }

Not documented yet.

public double FollowerStepSeconds { set; }

Not documented yet.

public SolverContext Solver { get; }

This simulation’s solver context, for benches that read ITS tape and profiler counters rather than some other simulation’s. The handle-free KLU_Tape_Stats / KLU_Profile_Read report the default context, which no simulation uses any more, so they would read zero.

public bool UseSolverKernel { get; set; }

Per simulation switch, defaulting to the process-wide one; tests A/B against KLU with it.

public string SolverKernelBackend { get; set; }

“native” or “il”; null takes the process-wide default. Tests A/B the two backends with it.

public long KernelSolveCount { get; }

Solves served by the generated kernel, builds performed, and the last build report.

public int KernelBuildCount { get; }

Not documented yet.

public string LastKernelReport { get; }

Not documented yet.

public double TargetFrequency { get; set; }

Not documented yet.

public bool Unthrottled { get; set; }

Not documented yet.

public double CatchUpForgivenSeconds { get; }

How many sim seconds of schedule have been written off since the last anchor (diagnostics).

public bool DevicePaced { get; }

True while the throttled loop is pacing against the device clock (diagnostics).

public bool AutoThrottle { get; set; }

Not documented yet.

public double EffectiveFrequency { get; }

The rate the throttled loop actually paces to — equals TargetFrequency unless auto-throttle has stepped down (HUD shows both when they differ).

public double ElementDtCeiling { get; }

The tightest element-declared BaseCircuitLogic.MaxTimeStep in the analyzed circuit, floored at 100 ns and capped at the 1 ms integration ceiling. Recomputed per Analyze; both throttle branches clamp TimeStep to it so a microcontroller is never handed a step bigger than its cycle budget (which it would silently truncate — the unthrottled “sim collapses at high clock” spiral).

public bool DebugDumpEnabled { get; set; }

Master switch for the diagnostic dumps (topology after Analyze, voltages on the next Tick). Written through DebugLog to CRUMB’s log. Set false to silence the spam.

public bool IsRunning { get; }

Not documented yet.

public ICircuitLogic[] Elements { get; }

Not documented yet.

public System.Action<string> DebugLog { get; set; }

Not documented yet.

public int ApiMisuseCount { get; }

How many times an element called a stamping/declaration API with arguments the engine had to refuse. Anything above zero is an authoring bug in a part.

public int NewtonPassesLastTick { get; }

Newton passes the last tick actually used.

public int NewtonPassesWorst { get; }

Most passes any tick has used since the run started.

public int UnconvergedTickCount { get; }

Ticks that ran out of Newton passes without converging, and were published anyway.

The pass loop exits either because everything converged or because it hit the budget, and until this counter existed those two were indistinguishable from outside. The published answer in the second case is whatever the last pass happened to produce, which for a part that is still moving is not a solution to anything. It presents as a component doing something physically impossible for one tick, with nothing reporting a problem.

public string LastUnconvergedElement { get; }

The element being stepped when a tick last ran out of passes.

public string LastApiMisuse { get; }

The most recent misuse message, for tests and error surfaces.

public int DiscoveredPairCount { get; }

How many distinct dynamic pairs were DISCOVERED mid-run rather than declared up front. Legal, self-healing, and each one costs an aborted tick - so a release part wants zero, but it is a cost, not a bug.

public int ReanalyzeCount { get; }

How many ticks were aborted into a mid-run re-Analyze by a dynamic stamp that was never declared. Each one resets every element on the bench, including any MCU, which silently reboots - so a part wants this to be zero.

public bool HasConverged { get; set; }

Global flag indicating if the non-linear elements have converged. Elements should set this to false during Step() if their values change significantly.

public string ConvergenceBlocker { get; }

The element that last cleared HasConverged, i.e. the part still moving when the passes ran out. Null until a tick fails to converge.

public int ConvergenceClears { get; }

How many times elements cleared HasConverged during the last unconverged tick. A number near the pass count means one part is moving every pass.

public int ConvergenceClearsThisTick { get; }

Not documented yet.

public string ConvergenceReason { get; }

Which stamp held the last unconverged tick open - see ConvergenceClearTag.

public int SubIterations { get; }

Not documented yet.

public bool NeedsAnalyze { get; set; }

Flag indicating that the topology has changed and MatrixMesh needs to be rebuilt.

public bool NeedsDynamicReanalyze { get; set; }

Not documented yet.

public int RunEpoch { get; }

Increments every time sim-time resets to 0 (ResetElements — a sim stop→start). Consumers holding time-stamped history compare epochs to know their capture belongs to a previous run and must be dropped.

public int LastPowerCycleFailures { get; }

Not documented yet.

public int FailureVersion { get; }

Bumped every time a component failure latches or failures are cleared. The host polls this from its frame loop and snapshots the failed set on movement — the same lock-free version pattern as the framebuffer/EEPROM mirrors. Written on whichever thread latches (Interlocked), read anywhere (Volatile).

public double ShortCircuitCurrentThreshold { get; set; }

Engine-side short-circuit detection: a voltage source whose post-convergence branch current exceeds this (amps) fails with reason “short circuit” — a dead short through an ideal source, far above any legitimate bench current. Zero or negative disables the check. Per-component limits stay USER-defined via BaseCircuitLogic.CheckFailure.

public bool FailureChecksEnabled { get; set; }

Master switch for the per-tick failure sweep — authored BaseCircuitLogic.CheckFailure AND the engine’s own short detection. ON by default. Off skips the sweep entirely: nothing can latch, and an already-latched component stays latched (and stays neutralised) rather than silently healing. The perf escape hatch for a circuit the user has already proven.

public int ShortCircuitDebounceTicks { get; set; }

Consecutive over-threshold ticks required before the short-circuit detection latches. Companion-model seeding after ANY re-Analyze legitimately draws a huge inrush for a tick (an ideal capacitor straight across a battery ≈ V·C/dt amps) — a single converged tick must never blow a healthy source.

public double ShortCircuitDwellSeconds { get; set; }

How long (SIM SECONDS) a source must sit over ShortCircuitCurrentThreshold before it latches. Time-based, not tick-based, because the transient this must ride out is physical: a bulk capacitor that starts EMPTY, at power-on or when it is placed on a rail that is already running, draws a genuine multi-millisecond recharge current (τ·ln(I₀/threshold): ~0.6 ms for 1000 µF through a regulator, ~6 ms for 10 mF). An Analyze is not such an event: the shipped capacitors keep their charge across its Reset (capacitor_common), and so do the MCU boards’ own (RailRegulator); only the engine’s test capacitors forget it. Its DURATION is fixed by the physics; its tick COUNT is not, so a tick debounce silently means different things at 1 kHz and 100 kHz.

The dwell is deliberately long and costs nothing: a genuine short is sustained INDEFINITELY, so waiting 50 ms only delays an inevitable latch by a hair of sim time, while a legitimate inrush is always over long before it. The MCU brown-out’s dwell rides out the same recharge from the board’s side (McuBoardProfile.BrownoutDwellSeconds, reasoned in MicrocontrollerElement’s supervisor comment).

This matters far more now the default threshold is bench-realistic (1 A) rather than near-mathematical (75 A): at 75 A only a single seeded tick ever crossed it, so 5 ticks was enough. At 1 A an ordinary Arduino + 1000 µF rail cap crosses it for ~64 ticks at 100 kHz, and blew its own supply until this became time-based.

public string SolverError { get; }

Human-readable description of why the circuit cannot be solved, or null while it can. Written on the sim thread, read anywhere (volatile reference assignment).

public int SolverErrorVersion { get; }

Bumped whenever SolverError appears or clears — poll it exactly like FailureVersion.

public int SolverErrorDwellTicks { get; set; }

Consecutive failed solves before the error is reported. Small — unlike a current transient there is no legitimate reason for a converged tick to be singular, so this only suppresses flicker across a re-Analyze, not a physical inrush.

public bool HaltOnFailure { get; set; }

“Pause on fail”: when true, the worker stops ticking (the run halts) as soon as any failure latches. The latch and neutralisation survive the halt — pressing Play resumes with the component still failed. The worker cannot pause-fence itself, so the halt is a plain run-stop the host’s run-state polling picks up.

public bool HaltedOnFailure { get; }

True when the worker’s most recent stop was the HaltOnFailure auto-halt (as opposed to a user pause). Lets the host’s “Reset failures” action resume the run without guessing from run-state timing. Cleared by the next StartSimulationThread and by Clear.

public bool LastSolveFailed { get; }

True while the most recent tick’s KLU solve FAILED (genuinely singular matrix — e.g. two ideal sources fighting). The element buffers hold the raw RHS for such ticks, not a solution: on a purely linear singular circuit nothing clears HasConverged, so the tick “converges” on garbage — this flag is the only reliable signal for that class. Cleared by the next successful solve.

public bool StampWireCurrents { get; set; }

Current View (OBS-1): when set, ideal wires are NOT collapsed at analyze - each spans two real matrix nodes joined by an engine-stamped WireStampResistance, so every wire carries a computable current I = (Va - Vb) / R read straight from the lead voltages the native scatter already publishes. Costs matrix growth (measured 2.8x tick time on the RAT board, WireStampCostBench); toggle under the pause fence and set NeedsAnalyze. Wires stay INERT either way - the stamp lives in the base matrix, never in per-tick element work.

public bool HasLeadSenses { get; }

Not documented yet.

public int NodeCount { get; }

Total number of unique matrix nodes in the circuit (including Node 0).

public int VoltageSourceCount { get; }

Total number of voltage sources in the circuit.

public int MatrixSize { get; }

The dimension of the MNA matrix.

public List<int> FloatingNodes { get; }

Nodes that have no path to Ground (Node 0) and require a 1E8 virtual resistor to prevent a singular matrix.

public NativeArray<double> CMatx { get; }

Not documented yet.

public NativeArray<double> CircuitRS { get; }

Not documented yet.

public NativeArray<double> CircuitRightSide { get; }

Not documented yet.

public ProbeManager Probes { get; }

The observability probe registry (Phase 1 spine). Samples bound element leads / voltage-source currents at the END of each tick into per-probe ring buffers that the multimeter / scope / audio consumers read off-thread. Lives outside the element set — a probe never adds a matrix node. Register/deregister from the frontend UNDER PauseSimulationWait/ResumeSimulation.

public FeedManager Feeds { get; }

The signal-feed registry — the probe spine’s INVERSE tap (audio-in). Advanced once at the START of each tick, before PreStep, publishing one sample per feed to its bound element’s BaseCircuitLogic.ExternalSignal — constant across all Newton sub-iterations. Same threading contract as Probes: Register/Rebind/Deregister from the frontend UNDER PauseSimulationWait/ResumeSimulation.

public WirelessField Field { get; }

The wireless medium — the over-the-air counterpart to the wired nets. Per-simulation, never static: two open circuits (or two parallel headless tests) must never hear each other, and the native solver’s global state means only one simulation ticks at a time anyway.

Unlike Probes and Feeds it holds NO registry — membership is derived by scanning the element set for components that declared a BaseCircuitLogic.FieldChannel, so it cannot drift out of step with a circuit that is being edited. Sending is safe from any thread; see WirelessField.

public double SimTimeSeconds { get; }

Not documented yet.

public int LiveWorkers { get; }

Not documented yet.

public int LiveWorkersMost { get; }

Not documented yet.

public double StabilityDtCeiling { get; }

The largest step the circuit has proven it can SOLVE, learned per run: quartered on an unconverged tick, halved when convergence needs a deep pass count, drifting back up while ticks stay clean. The SimulationLoop folds it into both branches like ElementDtCeiling - the sim dilates honestly instead of publishing garbage. Reset by Clear only: a re-Analyze keeps the learned edge, because the topology mostly survives an edit and re-learning it costs a garbage tick.

const

public const double FailedLeadResistance;

The neutralisation impedance: every lead of a FAILED component is tied to ground through this (the floating-node / MCU hi-Z precedent — a dead part reads like a dangling one). Public so tests and host heuristics share the single value.

const

public const double DevicePaceLeadSeconds;

How far sim production leads device consumption under device pacing. Small: the delivery cushions (resampler lag, ring fill) live OUTSIDE this lead and are preserved by the anchor, so this only covers report jitter.

const

public const double CatchUpForgiveSeconds;

Not documented yet.

const

public const double CatchUpKeepSeconds;

Not documented yet.

public double AutoThrottleFloorHz;

Auto-throttle never steps below this: 24 kHz keeps ~12 kHz audio bandwidth and clears the MCU’s MaxCyclesPerTick real-time bound (4 kHz at 16 MHz) six times over.

public double TimeStep;

Not documented yet.

public volatile int TicksPerSecond;

Not documented yet.

public bool ProfilingEnabled;

Not documented yet.

const

public const double DefaultShortCircuitThreshold;

The out-of-the-box short-circuit threshold every new circuit starts at (amps). Deliberately near bench reality rather than near the maths: a hobby rig that genuinely pulls this much is rare, so a source exceeding it is almost always a wiring fault. The host exposes it per circuit (persisted) because “almost always” is not “always” — a large TTL board or a bench supply legitimately draws more, and those circuits raise it.

const

public const double BusTransparentMaxOhms;

Series resistance up to this is bus-transparent (a logic level survives it).

const

public const double WireStampResistance;

The stamped wire resistance for Current View. 0.01 ohm: electrically negligible (bench-verified identical circuit behaviour), comfortably bus-transparent, and a round constant the render side divides by to recover current from the lead voltage delta.

public long UartRecursionDrops;

Deliveries abandoned because the reply chain ran too deep. COUNTED rather than silent: an echo storm that just stops looks exactly like a protocol bug.

public long UartFramingMismatches;

Bytes delivered between two ends whose declared framings disagree on baud, data bits or parity - the disagreements a real link garbles on. Counted, never acted on, in this version: the byte is delivered exactly as before, and this number is what has to read zero across every shipped sketch before a mismatch is allowed to corrupt anything. Stop-bit disagreements are recorded in LastUartFramingMismatch and do not count: a one-stop transmitter into a two-stop receiver works on a bench.

public int LastUartFramingMismatch;

The UartFraming mismatch flags of the most recent disagreement of any kind, for a test or a diagnostic to name the field.

public long PacketRecursionDrops;

Packets abandoned because the reply chain ran too deep. Counted, never silent.

public long PacketReceiverFaults;

Receivers that THREW out of OnPacket and had it swallowed. Non-zero means a node on the bus is faulty rather than absent — the difference between “nobody heard it” and “somebody heard it and fell over”, which every other counter reports identically.

public long I2CAddressCollisions;

How many times two devices answered one address. Monotonic for the run.

public long SpiSelectCollisions;

How many times two devices were selected on one SPI bus at once.

public BusCollision[] BusCollisions;

One record per (master, address) clash. Swapped whole, never mutated in place.

const

public const int FailureSweepStride;

The failure sweep’s MAXIMUM stride, in ticks. The sweep runs on every tick whose index is a multiple of the active stride - this value at audio-rate steps, backed off at coarse steps so one sweep window stays at or under 250 us - and for the length of the authored-check loop TimeStep reads as the whole covered window, so per-call dt integration in CheckFailure implementations stays time-accurate without any part changes. Power of two, so the gate is a mask. At 48 kHz this is a 167 microsecond check granularity against millisecond-scale overstress budgets.

public DynamicSingle GetMatrixEntryPointer(int i, int j);

A direct pointer to ONE matrix entry (row i, column j), for a stamp whose coefficient has to change during Step rather than only its right-hand side.

The working matrix is restored from its static state before every solve pass, so a value written here applies to that pass alone and never accumulates. Returns a no-op stamp when either row was eliminated during simplification, which keeps callers from writing through a pointer the solver does not own.

public DynamicConductance GetConductanceMatrixPointer(int i, int j);

Returns a DynamicConductance holding direct targets into the CCS Ax array (or the right-hand side, for a column held at a constant voltage) for stamping a conductance between nodes i and j. Node 0 (GND) is not in the matrix, so its entries come back with no target and are skipped. Must be called AFTER Analyze() completes.

public int GetRightSideIndex(int i);

Returns the mapped row index into the right_side array for node i. Returns -1 for node 0 (GND) — callers must guard against this.

public double* GetRightSidePointer(int i);

Returns a raw pointer directly to the right side array for node i. Returns null if the node is 0 or was dropped during simplification.

public void Analyze();

Not documented yet.

public unsafe void AttachTimeFollower(nint block, double stepSeconds);

Not documented yet.

public unsafe void DetachTimeFollower();

Not documented yet.

public void StampMatrix(int i, int j, double x);

Stamps a value into the matrix. A voltage change of dv in node j will increase the current into node i by x * dv. (Unless i or j is a voltage source node.)

public void StampRightSide(int i, double x);

Stamps a value on the right side of row i, representing an independent current source flowing into node i.

public void StampRightSideDynamic(int i);

Indicates that the value on the right side of row i will dynamically change during the Step() phase.

public unsafe void AddDynamicRightSide(int rsIndex, double x);

Fast inline helper for dynamically stamping the right-hand side during the Step() loop. This directly modifies the right_side pointer values without P/Invoke overhead and includes null/bounds guards. Use this instead of direct Simulation.RightSide[i] array access to avoid crashes.

public void StampNonLinear(int i);

Indicates that the values on the left side of row i will dynamically change during the Step() phase (non-linear).

public void StampCurrentSource(int n1, int n2, double i);

Stamps a fixed current source of i amps from n1 to n2: it draws the current out of node n1 and delivers it into node n2. Call it from StampInitial. For a current that changes every tick use StampCurrentSourceDynamic from Step instead.

Parameters

  • n1: The node the current is drawn from (a ConnectionNodes or InternalNode value).
  • n2: The node the current is delivered into.
  • i: The current, in amps.
public void StampVoltageSource(int n1, int n2, int vs, double v);

Stamps an independent voltage source (vs) from n1 to n2 with amount v.

public void StampVoltageSourceDynamic(int n1, int n2, int vs);

Use this if the amount of voltage is going to be dynamically updated in Step().

public void StampResistor(int n1, int n2, double r);

Stamps a fixed resistance of r ohms between n1 and n2. Call it from StampInitial; it is the same as StampConductance with 1 / r. For a value that changes while the circuit runs, declare the pair in StampInitial and stamp StampConductanceDynamic from Step.

Parameters

  • n1: One node of the resistor (a ConnectionNodes or InternalNode value).
  • n2: The other node.
  • r: The resistance, in ohms. Must be greater than zero.
public void StampConductance(int n1, int n2, double r0);

Stamps a fixed conductance of r0 siemens between n1 and n2. Call it from StampInitial. Prefer it to StampResistor when the value can be zero (an open circuit), which a resistance cannot express.

Parameters

  • n1: One node (a ConnectionNodes or InternalNode value).
  • n2: The other node.
  • r0: The conductance, in siemens (1 / ohms).
public void DefineConductanceUnknown(int i, int j);

Pre-allocates CCS matrix slots for a conductance between nodes i and j. Stamps PREPROCESS_VAL (1e-14) directly into the raw matrix, bypassing the node-0 guard in StampMatrix. This ensures the CCS structure contains entries for all four conductance positions so that GetMatrixPointers() can fetch valid pointers for direct writes during Step().

Must only be called during the Analyze phase (from StampDynamic).

public void StampVoltageControlledVoltageSource(int n1, int n2, double coef, int vs);

Voltage-controlled voltage source (VCVS). Control voltage source vs with voltage from n1 to n2. (Must also call StampVoltageSource or StampVoltageSourceDynamic).

public void StampVoltageControlledCurrentSource(int cn1, int cn2, int vn1, int vn2, double g);

Voltage-controlled current source (VCCS). Current from cn1 to cn2 is equal to voltage from vn1 to vn2, divided by g.

public void StampCurrentControlledCurrentSource(int n1, int n2, int vs, double gain);

Current-controlled current source (CCCS). Stamp a current source from n1 to n2 depending on current through vs.

public unsafe void StampConductanceDynamic(int n1, int n2, double g);

Stamps a conductance of g siemens between n1 and n2 for this solve pass. Call it from Step: it is the workhorse of switches, diodes, transistors and the companion models of capacitors and windings. Declare every pair you will stamp with DeclareDynamicConductance in StampInitial. An undeclared pair still works, but its first use aborts the tick into a re-Analyze that resets the whole bench.

Parameters

  • n1: One node (a ConnectionNodes or InternalNode value).
  • n2: The other node.
  • g: The conductance for this pass, in siemens. Zero leaves the pair open.
public void StampCurrentSourceDynamic(int n1, int n2, double i);

Stamps a current source of i amps from n1 to n2 for this solve pass, drawn out of n1 and delivered into n2. Call it from Step, typically beside StampConductanceDynamic as the history term of a capacitor or winding. Mark both nodes with StampRightSideDynamic in StampInitial so the injected current survives the matrix simplification.

Parameters

  • n1: The node the current is drawn from.
  • n2: The node the current is delivered into.
  • i: The current for this pass, in amps.
public void ReportDeviceClock(double simAtAnchor, double consumedSeconds);

Host audio sink: report the device’s consumption. Pass the sim time captured when this sink started consuming and the cumulative stream seconds consumed since. Thread-safe; call from the audio pump. Stops pacing (falls back to wall clock) when reports stop.

public void ClearDeviceClock();

Host audio sink teardown: stop device pacing immediately.

public void Dispose();

Releases this simulation’s native solver context. OPT-IN: nothing in the app calls it, and not calling it is exactly the behaviour that existed before contexts - the SafeHandle’s critical finalizer reclaims the context either way. Tests that build many simulations in a loop should call it so KLU’s factors are freed promptly rather than at the GC’s convenience.

The worker is stopped first, because Tick calls into the context with no other guard. That stop is best-effort (Join with a timeout), so the SafeHandle is the real protection: it refuses to release a handle that is currently inside a P/Invoke, and a call STARTED after disposal fails loudly as ObjectDisposedException rather than quietly reading freed memory.

public void AddElement(ICircuitLogic element);

Adds a compiled circuit logic component to the simulation loop.

public void RemoveElement(ICircuitLogic element);

Removes a circuit logic component from the simulation loop.

public void Clear();

Clears all elements from the simulation.

public void EnsureFieldDriveSlots(int slots);

Host bridge: grow the drive array to cover an id the host has just handed out.

public void ResetElements();

Resets the runtime state (voltages, currents, and accumulated per-element history) of every element back to zero without changing the circuit topology. Intended to be called only while the simulation thread is stopped — the frontend’s Reset button is paused-only — so this is a no-op guarded against a live sim thread to avoid racing Tick().

public void LatchFailure(BaseCircuitLogic element, string reason);

Latches a component failure: sets the element’s BaseCircuitLogic.Failed latch (and BaseCircuitLogic.FailureReason unless the author’s Fail(...) already set one), zeroes its exposed currents (they freeze once the element leaves the hot loops — a dead part must read dead), schedules the neutralising re-Analyze, and bumps FailureVersion. Called by the per-tick sweep and the Analyze-time short detection on the sim thread; hosts may call it while the sim is stopped or fenced.

public List<BaseCircuitLogic> SnapshotFailedElements();

Every currently-failed element, in registration order — the host maps these back to proxies for attribution (it owns proxy→element; invert by scan).

public void ClearFailures();

Clears every failure latch and schedules the re-Analyze that restores the real stamps — the designer’s “Reset failures” action. Runtime state only (never undoable). Call while the sim is stopped or under the pause fence, like every other topology mutation.

public bool Connect(ICircuitLogic leftElem, int leftLeadIdx, ICircuitLogic rightElem, int rightLeadIdx);

Connect two leads together, assigning them the same node ID. Handles all 4 cases: both unconnected, one unconnected, both connected (merge). Returns false when either element is not registered in the node mesh yet — callers MUST treat that as “not connected” (keep searching / SelfConnect), never as success. A silent void no-op here let AttachToSim mark leads as connected that never were, fragmenting nets in attach-enumeration order (the historic “sometimes breaks on move/attach” dice roll).

public void SelfConnect(ICircuitLogic elem, int leadIdx);

Self-connect a lead (assign it its own unique node ID if unconnected). Used when a component’s pin has no physical neighbor yet.

public long GetLeadNodeId(ICircuitLogic elem, int leadIdx);

Gets the node ID currently assigned to a lead. Returns -1 if unconnected.

public void DeclareLeadSense(BaseCircuitLogic logic, int leadIdx);

Declare a branch-current sense on one lead. Idempotent. MUST be called under the pause fence; takes effect at the next PushNodeMeshToElements + Analyze while StampWireCurrents is on (parked and inert otherwise).

public bool TryGetLeadSense(BaseCircuitLogic logic, int leadIdx, out LeadSenseElement sense);

The live sense for a declared lead, if any. Safe to read from the frontend thread at frame rate (the registry only mutates under the pause fence, on the same thread).

public void ClearLeadSenses();

Drop every declared sense (their elements included). MUST be called under the pause fence.

public long WireNetOf(long netId);

The net a lead is on with ideal wires collapsed - the same answer in both wire modes, and the EXACT identity of a pin. BusGroupOf is wider: it also unions nets joined through a bus-transparent series element (a 1 k protection resistor, a declared pass-through), which is the right question for “does this control line reach that pin” and the wrong one for “which pin is this” - a 1 k speaker between two GPIOs unions them too, and a group-keyed identity then answers the first pin for both (BUSG-1). Negative ids pass through.

public void RequestBusRegroup();

Re-read every part’s BaseCircuitLogic.BusTransparentConnections and rebuild the group table at the next tick boundary. A part asks for this through BaseCircuitLogic.NotifyBusTransparencyChanged when its own answer has moved with its state: a switch closed, a mux select changed, an enable went active.

WITHOUT IT THE GROUPS STAND UNTIL THE NEXT ANALYZE, and a bus through a switch the user has just closed stays dead while every voltage on the bench reads correct. Measured on a 2-terminal switch: the contact drops to 0.002499 V and BusNetsMatch is still false 200 ticks later.

AND THIS IS NOT AN ANALYZE AND MUST NOT BECOME ONE. An Analyze resets every element, re-mints every node id and refactorises the matrix - measured 2.7086 ms on a 201-element board against 0.1391 ms for the regroup, 19.5x - so a part toggling at firmware rate would reboot every chip on the bench, every toggle.

Latched, not immediate, and drained between PreStep and Step, so transparency is fixed for the whole tick and a switch bouncing mid-transfer can never re-route a live transaction. Safe from any thread: one bool store.

public bool IsTickPacedNet(long netId);

True when a master driving this net must stop until the next tick after each write. Null-set fast path: one reference compare when no device asked for pacing.

public long BusGroupOf(long netId);

The bus group a net belongs to (identity when nothing unions it). Negative ids (unconnected) pass through — callers must not treat two -1s as a match.

public bool BusNetsMatch(long a, long b);

True when two nets are the same bus — identical or joined through bus-transparent series resistance. Unconnected (negative) nets never match anything.

public int UartRxLeadOn(BaseCircuitLogic e, long txNet);

Which of e’s connections listens on txNet, or -1.

Resolves the MULTI-PORT declaration first: a component that overrides BaseCircuitLogic.UartRxConnections is answered entirely from that array, and the single-port BaseCircuitLogic.UartRxConnection is then ignored — one declaration is authoritative, so a part can never half-listen through two surfaces. Multi-port is what a real transceiver needs (an RS-485 driver bridges a controller-side pair to a bus-side pair) and what a real board has (an ATmega2560 carries four serial ports).

public BaseCircuitLogic FindUartReceiver(BaseCircuitLogic sender, int senderTxLead);

Find A component listening on senderTxLead’s net — first match only. Kept for callers that want a single peer handle to cache; DeliverUartByte is what actually carries traffic and reaches EVERY listener.

UART is CROSSED, so this matches TX to RX: a module that merely transmits (a GPS) works exactly as it does in hardware with nothing wired back. A FAILED peer is invisible, the same engine-side gate FindI2CSlave applies, so no authored device writes its own !Failed check. Null means nothing is wired. Sim-thread only.

public bool HasUartReceiver(BaseCircuitLogic sender, int senderTxLead);

True when anything — a plain component or a microcontroller — would hear a byte sent from this lead.

public int PacketBusLeadOn(BaseCircuitLogic e, long busNet);

Which of e’s connections sits on busNet, or -1.

public bool HasPacketPeer(BaseCircuitLogic asker, int connectionIdx);

Is any other node on this net? The reachability question a protocol asks before transmitting, so it can report an honest failure rather than assuming success.

public void ClearBusCollisions();

Drop every collision record. A repaired wire has to stop reporting, so this runs wherever the other per-run bus caches die.

public BaseCircuitLogic FindI2CSlave(BaseCircuitLogic master, int masterSdaLead, int masterSclLead, int address);

Find the I2C slave answering address on the master’s SDA/SCL nets, or null (→ NACK, like an empty bus). A FAILED slave is invisible here — a burnt-out part must go silent on the bus, not just lose its stamps (see LatchFailure). Gating it in the engine keeps the `!Failed` check out of every authored device.

Callable from the sim thread or from a chip/pacer thread: everything it reads (the node mesh, the element cache, the bus-group table) is rebuilt only in Analyze, which hard-quiesces every element pacer first. It reads converged voltages and user-authored virtuals concurrently with the solve; both are point-in-time reads by design.

public BaseCircuitLogic FindSpiSlave(BaseCircuitLogic master, int masterSckLead, int masterMosiLead, Func<BaseCircuitLogic, int, bool> isSelected);

Find the SELECTED SPI slave on the master’s SCK net. SCK nets must match; chip-select is decided by isSelected — the master supplies it because a CS driven by the master’s OWN GPIO must use the LIVE mid-tick pin state (firmware toggles CS and transfers within one tick; the solved node voltage lags a tick), while an externally-driven CS falls back to the converged voltage (< BusLowThreshold). A slave that DECLARES its data pin (BaseCircuitLogic.SpiMosiConnection) must also have it wired to the master’s MOSI — clock alone carries no data, exactly like hardware; undeclared legacy devices route on SCK + CS alone. A FAILED slave is invisible here (a burnt-out part goes silent on the bus, not merely un-stamped — the same engine-side gate as FindI2CSlave). Null → MISO reads 0xFF. Same threading contract as FindI2CSlave: sim thread or a chip/pacer thread, against tables that only Analyze rebuilds.

public bool IsBusPinLow(BaseCircuitLogic slave, int lead);

The externally-driven CS fallback: the slave’s converged CS node voltage reads low. Bounds-guarded — lead indices come from free-form user virtuals and the voltage buffer is raw native memory.

public bool SenseBusPinIsHigh(BaseCircuitLogic device, int lead);

The digital level of one of device’s pins, transaction-aware: inside a bus callback a control line driven by the transacting master’s own GPIO (an SPI display’s D/C, for example) reads its LIVE mid-tick state — firmware toggles such lines and transfers within one tick, so the converged node voltage lags. Everywhere else (or when the master doesn’t drive the net) it falls back to the converged electrical level.

public void PushNodeMeshToElements();

Pushes all node mesh IDs to the elements’ SetConnectionNode, mapping snowflake IDs to sequential integer node IDs for the matrix. Called before Analyze().

public void ResetNodeMesh(ICircuitLogic element);

Resets all node mesh entries for a given element back to -1 (unconnected). Called during DetachFromSim before re-attaching.

public void ResetSharedLeads(ICircuitLogic element);

Not documented yet.

public void Initialize();

Initializes the simulation matrix. Called once before the simulation begins or whenever the circuit topology changes.

public bool StartSimulationThread();

Not documented yet.

public void StopSimulationThread();

Not documented yet.

public bool WaitUntilStopped(int ms);

Not documented yet.

public void PauseSimulationWait(int timeoutMs = 1000);

Not documented yet.

public void ResumeSimulation();

Not documented yet.

public unsafe void Tick();

The main tick loop. This advances the simulation forward by one fixed step.

public string TakeProfileReport();

Take the accumulated tick profile as a string and reset the accumulators.

The report is normally published once a second from the run loop, which a HEADLESS BENCH never enters - it calls Tick() directly, so the accumulators filled up and were never read. That made the one piece of per-phase instrumentation in the engine unavailable from exactly the place perf questions get answered. Returns null when no ticks have been profiled.

Set ProfilingEnabled before ticking, then take the report afterwards.

Generated from the CRUMB source, commit fcec660d, 2026-10-29.