Skip to content

BaseCircuitLogic

public abstract unsafe class BaseCircuitLogic : ICircuitLogic, IDisposable

Represents the base abstract class for implementing circuit logic in a simulation system. This class provides fundamental methods and properties to support the management of circuit connections, voltages, and related behaviors. Derived classes must implement abstract methods to define specific circuit behavior and interactions.

virtual

public virtual bool RequiresGlobalDynamicCache { get; }

Indicates if this component relies on the global jagged array cache for dynamic stamping. If false, the simulator can skip allocating and checking the global cache for this component. Overridden by the AST Rewriter.

virtual

public virtual uint VoltageSourceCount { get; }

Represents the number of voltage sources associated with the circuit logic. This count is critical for determining the number of independent voltage equations in the circuit’s mathematical representation.

virtual

public virtual uint TransconductanceCount { get; }

How many voltage-controlled current sources this part drives per solve pass. Declare each one in StampInitial with DeclareTransconductance and set it in Step with SetTransconductance.

virtual

public virtual uint InternalNodeCount { get; }

PRIVATE matrix nodes this element owns, beyond its physical pins — override to claim some. Internal nodes live in lead slots PAST ConnectionCount, at [ConnectionCount, ConnectionCount + InternalNodeCount). They are never entered into the node mesh, so each one lands on an unconnected lead and is assigned a node of its own.

Use one whenever a model needs a junction that isn’t a pin — the textbook case being a source with series impedance (a cell’s ESR, a regulator’s output resistance):

public override uint InternalNodeCount => 1;
public override void StampInitial()
{
// ideal source drives the private node, then resist out to the real + pin
Simulation.StampVoltageSource(ConnectionNodes[0], InternalNode(0), VoltageSources[0], 9.0);
Simulation.StampResistor(InternalNode(0), ConnectionNodes[1], 2.0);
}

A 2-terminal source + ONE series resistor can also be written without an internal node, via the Norton transform (current source V/R in parallel with R) — but that trick does not generalise to 3+ terminal devices, to two elements in series inside a part, or to an internal node with 3+ attachments, which is what this is for.

Costs exactly one matrix node each, and only for what you declare. Their voltages are solved and written back like any lead, readable via ConnectionVoltages at the same index. They carry NO current slot (currents are per voltage-source / per pin) and are never bus leads.

virtual

public virtual double MaxTimeStep { get; }

The largest integration timestep this element can honestly step, in seconds. The sim clamps its variable/derived timesteps to the tightest element’s declaration (see the SimulationLoop throttle branches): an overloaded sim then DILATES instead of handing elements steps they must silently truncate. The canonical case is a microcontroller’s cycle budget — a step above MaxCyclesPerTick/clockHz forces it to DROP cycles, so firmware time falls behind sim time while everything looks “real time”. Infinity = no requirement (analog elements integrate any dt).

virtual

public virtual int I2CAddress { get; }

The 7-bit I2C address this component answers to, or -1 (default) when it is not an I2C device. A master’s transaction only reaches this component when the address matches AND its I2CSdaConnection/I2CSclConnection pins share nodes with the master’s SDA/SCL. Tip: return a property (e.g. (int)Properties.Address) to make the address editable per placed instance.

virtual

public virtual double BusInputLowVolts { get; }

Below this, an input pin on this device reads LOW. Default 1.5 V.

Return a property to make it editable per placed instance, the same way I2CAddress does. A device that declares a real pair - 0.8 and 2.0 for TTL, 0.3 and 0.7 of the rail for CMOS - gets Schmitt behaviour: the level only changes when the voltage clears the far threshold, and holds through the band in between. Declaring a high that is not above the low leaves the single-threshold behaviour in place.

virtual

public virtual double BusInputHighVolts { get; }

At or above this, an input pin on this device reads HIGH. Default 1.5 V.

virtual

public virtual int UartRxConnection { get; }

The connection index this component RECEIVES on, or -1 (the default) when it does not listen. Bytes transmitted onto this net arrive at OnUartByte.

virtual

public virtual int UartTxConnection { get; }

The connection index this component TRANSMITS from, or -1 (the default).

virtual

public virtual int[] UartRxConnections { get; }

Every connection index this component receives on, or null (the default) to use the single UartRxConnection. When non-null this array is the whole answer.

virtual

public virtual int[] UartTxConnections { get; }

Every connection index this component may transmit from, or null (the default) to use the single UartTxConnection. Declared so UartHasPeer can answer for a named port; sending itself always names its port explicitly.

virtual

public virtual int UartBaud { get; }

The baud this part’s port is set to, or -1 (the default) for “any” - the port follows whatever talks to it, which is how a module with auto-baud or a device whose rate is its own business is described.

virtual

public virtual int UartDataBits { get; }

Data bits per frame, 5 to 9. Eight by default.

virtual

public virtual int UartParity { get; }

Parity: UartFraming.ParityNone (the default), ParityEven or ParityOdd.

virtual

public virtual int UartStopBits { get; }

Stop bits: UartFraming.StopOne (the default), StopTwo or StopOneAndHalf.

protectedvirtual

protected virtual bool UartRelaysFraming { get; }

True for a part that RE-SENDS bytes it heard rather than originating them - a level shifter, an RS-485 or RS-232 transceiver, a buffer. A level shifter does not reframe: what leaves it is exactly what entered it, at whatever framing the originator used. So a relay declares nothing on the bytes it forwards and the originator’s framing rides through untouched. Left false, a MAX485 carrying an 8E1 Modbus stream would stamp its own default 8N1 onto every byte and the far end would count a parity mismatch on a link that works.

virtual

public virtual int[] PacketBusConnections { get; }

Connection indices this component sits on as a packet-bus node, or null (the default) when it is not one. A node both sends and receives on the same lead — a shared wire is not crossed the way a UART pair is.

virtual

public virtual int[] BusTransparentConnections { get; }

Connection index PAIRS this component passes a logic level straight between, flattened: { a0,b0, a1,b1, ... }. Declaring a pair tells the engine those two nets are THE SAME BUS, so a master on one side finds a device on the other.

This exists because a small series RESISTOR is transparent automatically (the engine detects it), but an ACTIVE pass-through cannot be inferred — a MOSFET could be a switch, an amplifier or anything else, and only the author knows it is meant to pass a bus through. Declaring it covers a whole class of real parts with no engine work for any of them: bidirectional level shifters (the BSS138 board every 5 V-to-3.3 V I2C hookup uses), buffers and line drivers (74HC245), bus repeaters, opto-isolators, analog switches and muxes.

The pair is symmetric and transitive — chain two shifters and the whole path is one bus. A FAILED component passes nothing (a burnt-out shifter breaks the bus, as it should), and a pair touching a power or ground net is ignored, because a bus that swallowed a rail would put every device in the circuit on every bus.

A mux, a switch or an analog gate can return different pairs as its state changes: return the pairs that are CONDUCTING RIGHT NOW, and call NotifyBusTransparencyChanged when the answer moves. The engine re-groups at the next tick boundary. Do NOT declare a pair that merely COULD conduct: an open throw, an unselected channel and a reverse-biased junction pass no logic level and must stay opaque.

// a 4-channel level shifter: LV0..LV3 pass through to HV0..HV3 public override int[] BusTransparentConnections => new[] { Connections.LV0, Connections.HV0, Connections.LV1, Connections.HV1, };

virtual

public virtual int FieldChannel { get; }

The channel this component transmits and listens on. Return -1 (the default) to stay off the field entirely. Any two components sharing a channel in the same circuit can hear each other. Tip: return a property (e.g. (int)Properties.Channel) to make it settable per placed instance.

virtual

public virtual byte[] FieldAddress { get; }

The address this component answers to, or null to receive only broadcasts. A unicast reaches every endpoint whose address matches byte-for-byte.

virtual

public virtual int I2CSdaConnection { get; }

The connection index (see the generated Connections class) of this device’s SDA pin. Required (with I2CSclConnection) for I2C transactions to route here.

virtual

public virtual int I2CSclConnection { get; }

The connection index of this device’s SCL pin.

virtual

public virtual int SpiCsConnection { get; }

The connection index of this device’s chip-select (CS/SS) pin, or -1 (default) when it is not an SPI device. An SPI transfer only reaches this component when its SpiSckConnection shares a node with the master’s SCK AND this CS pin is electrically LOW at transfer time — drive it from any master GPIO, exactly like hardware.

virtual

public virtual int SpiSckConnection { get; }

The connection index of this device’s SCK pin. Required for SPI routing.

virtual

public virtual int SpiMosiConnection { get; }

The connection index of this device’s MOSI/SDA (data-in) pin, or -1 (default). Hardware SPI routes without it; declaring it additionally enables SOFTWARE (bit-banged) SPI — a sketch toggling arbitrary GPIOs (shiftOut, Adafruit’s software-SPI constructor) is decoded engine-side at CPU-cycle resolution and delivered through the same OnSpiTransfer byte contract (mode 0, MSB-first, write-only — the return byte is discarded because a bit-banged master samples MISO electrically, which the transaction-level bus does not drive).

⛔ DECLARE THIS OR TickPacedConnections, NEVER BOTH. Bit-banged decode hands a device whole bytes; tick pacing keeps every transition in the circuit one tick at a time. A part that declares both is decoded into bytes on the families that decode, and a device handed a whole byte never moves its serial output bit by bit - so the far chip of a 74HC595 cascade reads nothing while the near chip looks perfect. That is how it would ship. Decode is for a device that consumes bytes; pacing is for one whose output is itself a serial line.

virtual

public virtual int SpiMisoConnection { get; }

The connection index of this device’s MISO (data-out) pin, or -1 (default). Declaring it tells the engine that the master can only HEAR this device when that pin is genuinely on the master’s MISO net: delete the wire and the reads come back 0xFF, exactly as they do on a bench. A device that declares nothing answers on SCK and CS alone, which is what every device did before this existed.

The device still SEES every byte either way. Write-only wiring is ordinary - an ST7735 with MISO left floating is how those panels are usually fitted - and on silicon its shift register advances whether or not anyone is listening. Only the master’s read is gated.

It does not make bit-banged readback work. A software-SPI master samples MISO electrically, and the transaction-level bus does not drive that net.

virtual

public virtual int EdgeSensitiveConnection { get; }

The connection index of this device’s single-wire DATA pin (a NeoPixel’s DIN, a DS18B20’s DQ, a DHT’s DATA), or -1 (default) when not edge-sensitive. When an MCU GPIO shares this pin’s bus group (net identity through bus-transparent series resistors, the same routing as SPI/I2C), every change of the resolved line level arrives at OnConnectionEdge at CPU-cycle resolution.

The line’s level is the circuit’s. While the master drives it, it is the master’s level, and while this device holds it, it is low. While neither does, it is what a solve of the circuit finds with the master’s pad let go in exactly its current pull state: the line’s pull-up if it has one, the pad’s own pull, this device’s own stamps, a button or a second driver. A line with no pull-up reads low when it is let go, as it would on a bench. When a change (the master’s pinMode or pull write, or this device’s release) lets the line go into a state the circuit has not been solved in yet, the master’s chip stops at its next instruction until it has, and the edge is delivered stamped with the cycle of the change, so the firmware’s next read and this device’s edge are exact in the master’s clock at any tick rate. The stop costs the chip no guest time: it keeps the rest of its budget and runs it once the solve is in, so its clock stays with the sim’s. On a Pico that holds for either core: while any bound device can drive one of its edge lines (one that does not declare EdgeLinesListenOnly, or one that drove after declaring it), the board runs its two cores in step, each within a slice (64 cycles), plus one pass of a compiled loop, of the other. A board whose edge devices only listen runs its second core on its own thread, and there what that core does on a line is exact only to within a run where it depends on the first core’s clock: its timer reads, a PIO program it feeds, a read of a line the other core drives (see the EdgeBridge header). Three more limits, set by where the board looks: a compiled firmware loop sees this device’s edges at its next pass and reads a release its own store made at the old level for the rest of that pass; a Pico’s PIO is stepped after each 64-cycle slice of core 0, so core 0 can read a line up to a slice past a PIO change, and a PIO program that lets a line go and samples it in one batch reads the old level; and a pinMode that passes through an unpulled state on its way to INPUT_PULLUP hands this device a short low the wire, which holds its charge, never makes, if nothing else pulls the line up (see the EdgeBridge header). A change the circuit makes while the line is let go (a button, a pull-up added, an RC charging) reaches the firmware and this device when the board next samples its pads: every tick on a board in step with the sim, and on one that runs ahead, the first quantum in which neither the chip nor this device moves the line, or, while they are moving it, the first release after the next exchange, which waits for a solve (a quantum). Nothing else on the net moves a line the master or this device is driving; scopes still show the solved voltage. Several devices may share one line, each receiving every edge the master or another device makes on it: an amplifier and a microphone on one bit clock, a probe on a data line a microphone drives. A device is never handed its own drives. Two devices driving one line at the same time resolve as a wired-AND (any hold reads low) and nothing models the contention beyond that, so give a line one driver at a time.

virtual

public virtual int[] EdgeSensitiveConnections { get; }

Every edge-sensitive connection this device owns, when ONE is not enough — an HC-SR04 has a TRIGGER it listens on and an ECHO it drives, and a device that could declare only one of them was not merely incomplete, it was unauthorable. Return null (the default) to keep using EdgeSensitiveConnection alone, which is what almost every device wants; a two-wire part overrides this instead and leaves the singular at -1.

Each listed connection binds independently: edges arrive at OnConnectionEdge tagged with the connection they came from, and ScheduleConnectionDrive, ScheduleConnectionLevel, ConnectionDriveIsLow and ConnectionDriveIsHigh take a connection index. A line this device drives must be declared here too: a drive reaches the master only through a bound line.

virtual

public virtual bool EdgeLinesListenOnly { get; }

Not documented yet.

virtual

public virtual int[] TickPacedConnections { get; }

Connections whose driving MASTER must pace its writes to the simulation tick grid, or null (the default) for a device that needs no such help.

THE PROBLEM THIS SOLVES. A tick-latched device samples its pins once per tick. Firmware does not: shiftOut() on a 16 MHz AVR emits clock pulses 1-3 ticks wide and hardware SPI emits them far narrower, so most transitions begin and end BETWEEN ticks and the device never observes them. Measured on a 74HC595: 0x06 latched where 0xA5 was sent, and 0x00 on the faster two families. Silently the wrong byte, with every pin at a believable voltage.

WHY PACING RATHER THAN DECODING. The obvious alternative is to reassemble the bit-bang into whole bytes engine-side and hand them over (OnSpiTransfer). That works for ONE device and breaks a CASCADE: two ‘595s chained QH’->SER rely on the near chip moving its serial output bit by bit, and a chip handed a whole byte never does. Measured, and it broke a topology that had been working. Pacing keeps everything per-bit in the circuit, so cascades, ripple chains and single devices all behave the same way — which is also how real hardware behaves, only slower.

THE CONTRACT. A master that finds one of its output pins on a paced net must, after writing it, stop executing until the next tick. The cost is guest time: a burst that firmware thinks took 4 us occupies one tick per transition, so sim time and cycle time diverge across it. That divergence is already a documented property of this engine (see EdgeBridge’s header on the anti-runaway cap) rather than a new hazard, and it buys a stock tutorial sketch working unmodified against a part the user simply wired up.

DECLARE ONLY THE PINS A MASTER DRIVES. Marking a power rail would pace every unrelated write on the board. There is deliberately NO topology search: a device speaks for itself and says nothing about what it is chained to, so a chain of any depth needs no traversal — every link in it declares its own pins.

virtual

public virtual bool StampsFixedWithinTick { get; }

Declares that this element’s Step() stamps the same values on every Newton pass of one tick: nothing it stamps depends on the evolving solution, and it never clears HasConverged. The tick loop then skips it on validation passes, which only exist to let solution-dependent elements re-check their linearisation. A resistor network, a capacitor’s companion conductance, a pot at a per-tick position and a fed jack all qualify; anything that reads ConnectionVoltages inside Step does NOT. Opt-in: the default false is always correct, merely slower.

public CircuitSimulation Simulation { get; }

The simulation instance running this logic. Exposed to user scripts via a public getter, but safely assigned internally.

public double[] UserData { get; }

A generic state array for user scripts to share data between CircuitLogic and ComponentLogic without resorting to reflection or dynamic typing.

public double ExternalSignal { get; }

The current sample of a host-registered signal feed (the probe spine’s INVERSE tap — see FeedManager): audio-in, function-generator streams, anything the host pushes INTO the sim. Written by the sim worker once per tick BEFORE PreStep and therefore constant across every Newton sub-iteration of that tick — logic can read it in PreStep/Step and stamp it without any hold logic of its own. Reads 0.0 while nothing is bound (an unplugged jack is silent).

public long FramebufferVersion { get; }

Monotonic change counter for the framebuffer; 0 = never written. Frame side: compare with != (not <) — a hot-reloaded logic instance restarts at 0.

public long PersistentImageVersion { get; }

Monotonic change counter for the durable image; 0 = never written. Frame side: compare with != (not <) - a hot-reloaded logic instance restarts at 0.

public string PersistentImageProperty { get; }

The property name this component declared as its durable image, or null when it declared none. The frame side’s whole test for “does this part persist anything”.

public int PersistentImageLength { get; }

Length of the durable image, 0 when there is none.

public bool HasDurableIdentity { get; }

Whether the host claimed this element with a durable identity.

False on every headless bench, where the fallback is construction order. That fallback is fine for a tolerance - a resistor that is 1% high in an unclaimed rig is still a resistor - and NOT fine for anything that has to survive a reload or be unique across a circuit, because construction order changes with what else is in the circuit. A caller that needs the stronger guarantee asks for it here and keeps its own default when the answer is no.

generated for youabstract

public abstract uint ConnectionCount { get; }

CRUMB generates this from your Connections markers. Don’t declare it in your code.

Represents the total number of connections associated with the circuit logic. This property defines the maximum number of connection points available for the circuit component and is essential for initializing connection-specific data and managing circuit behavior.

protected

protected int* ConnectionNodes { get; }

Represents the array of connection nodes associated with the circuit logic. Each entry in the array corresponds to a unique connection point within the circuit, and the node values indicate how connections are mapped to the simulation environment. This property is central to managing circuit connectivity and node assignments.

protected

protected double* ConnectionVoltages { get; }

Represents an array containing the voltage values at each connection point within the circuit. This property provides access to the current voltage levels across all connection nodes, enabling computations related to circuit behavior, node voltage differences, and related operations.

protected

protected int* VoltageSources { get; }

Represents the indices of voltage sources associated with the circuit component. This property identifies and organizes voltage source connections, allowing for efficient access and management during simulation processes.

protected

protected double* Currents { get; }

Represents the collection of current values associated with the circuit’s internal components or voltage sources. This property is used to store and retrieve the calculated currents, enabling analysis of circuit behavior and aiding in system simulations.

public bool QuiesceRequested { get; }

True only for a PostTick invoked from a hard-quiesce site — the element must leave no spawned work running when it returns. False on the routine per-tick sweep.

public bool Failed { get; }

True once this component has FAILED — its CheckFailure returned true, or the engine detected a short circuit through it. A failed component is electrically neutralised: its stamps are skipped and every lead is tied to ground through 100 MΩ. The latch survives ordinary re-Analyzes (wire moves, value edits) and clears only on PowerCycle (the designer’s Reset-state button) or an explicit reset-failures action.

public string FailureReason { get; }

Why the component failed — the message set by Fail (or by the engine for built-in detections like “short circuit”), shown to the user verbatim. Null while healthy.

protected

protected int FieldPending { get; }

Frames waiting to be taken.

const

public const double DefaultBusThresholdVolts;

The one threshold every bus query used before the pair existed.

virtual

public virtual void CalculateCurrent();

Computes the instantaneous current values for the circuit element, from the solved connection voltages, and publishes them with SetCurrent.

Runs ONCE PER TICK, after the Newton loop, on the tick’s last solve, and never on a tick that aborted into a re-Analyze. On a converged tick that is the solution; on a tick that ran out of passes or whose solve failed it is not, and the engine flags those (UnconvergedTickCount, LastSolveFailed). That makes it the place to advance per-tick state such as a capacitor’s charge, and a point where a published value is never a mid-loop trial. It runs after the tick’s last Step: compute from ConnectionVoltages and state latched in PreStep rather than from fields Step rewrites every pass, because on a converged tick Step has run once more, on the final solution, before this call. (Until 2026-09-23 this ran after every solve pass, and a capacitor that added its charge here charged several times per tick.)

virtual

public virtual double GetCurrent(int voltSourceIdx);

Retrieves the current value associated with the specified voltage source.

Parameters

  • voltSourceIdx: The index of the voltage source for which the current is to be retrieved.

Returns The calculated current value as a double.

virtual

public virtual void SetCurrent(int voltSourceIdx, double current);

Sets the current for a specified voltage source in the circuit. This method assigns a precise current value, measured in amperes, to the voltage source identified by its index.

Parameters

  • voltSourceIdx: The index of the voltage source to which the current will be assigned.
  • current: The current value to be set for the specified voltage source, in amperes.

virtual

public virtual bool ConnectionsAreConnected(int idxLeft, int idxRight);

Determines whether two circuit connections are electrically connected. This may include checking for a direct physical connection or a connection established through circuit components.

Parameters

  • idxLeft: The index of the first connection to check.
  • idxRight: The index of the second connection to check.

Returns True if the connections are electrically connected; otherwise, false.

virtual

public virtual bool IsConnectionGround(int idx);

Determines whether the specified connection index corresponds to a ground connection within the circuit logic. A ground connection typically indicates a reference point in the circuit with zero potential.

Parameters

  • idx: The index of the connection to be evaluated.

Returns A boolean value indicating whether the connection is a ground connection. Returns true if the connection is ground; otherwise, false.

virtual

public virtual double GetVoltageDelta(int idxLeft, int idxRight);

Calculates the voltage difference between two circuit connections identified by their respective indices. This method determines the voltage delta by subtracting the voltage of the second connection from the voltage of the first connection.

Parameters

  • idxLeft: The index of the first connection.
  • idxRight: The index of the second connection.

Returns The voltage difference between the specified connections.

virtual

public virtual bool IsWire();

Determines whether the circuit connection behaves as a wire. This method identifies connections that facilitate direct transmission of voltage without resistance, delay, or other electrical effects typically associated with circuit components.

Returns Returns true if the connection is a wire; otherwise, false.

virtual

public virtual void OnRemovedFromSimulation();

Called once when the element leaves the simulation (RemoveElement or Clear) — the place to stop owned threads or release native resources. NOT called on Reset/Analyze (the element is still in the circuit there). Default: nothing.

virtual

public virtual void Reset();

Resets the circuit state by clearing all connection voltages and currents. This method initializes connection voltages to zero and resets all voltage source currents to their default state, ensuring the circuit is in a neutral starting condition.

virtual

public virtual void PowerCycle();

Explicit whole-run restart — the designer’s “Reset state” button, NOT the mechanical per-Analyze Reset. Full power-on semantics: everything Reset() clears plus device state that must survive ordinary topology changes (a display’s pixels, a microcontroller’s program state). Override for extra power-on behaviour; always call base.

virtual

public virtual bool CheckFailure();

Checks whether this component is failing — override it to give the component realistic limits (over-voltage, over-current, reverse polarity…). The engine calls it on the converged solution, so ConnectionVoltages and GetCurrent hold real values here. The sweep is DECIMATED — up to every CircuitSimulation.FailureSweepStrideth tick at audio-rate steps, backing off to every tick at coarse steps, with the sampled phase walking so tick-locked waveforms cannot hide between samples — and for the length of the sweep CircuitSimulation.TimeStep reads as the whole window a call covers, so keep integrating dt per call, exactly as below, and the stress stays accurate in simulated seconds. Return true to FAIL the component: the failure latches, the part is electrically neutralised (all leads go high-impedance to ground, like a burnt-out part), and the user is told. Debounce through AccumulateOverstress so a brief transient (or the zeroed readings right after a re-Analyze) cannot blow the part — see the pattern below — and call Fail just before returning true to say WHY it failed.

private double _stressSeconds;
private int _stressTicks;
public override bool CheckFailure()
{
// Read the supply DIFFERENTIALLY — never assume the GND lead sits at 0 V.
double vcc = ConnectionVoltages[Connections.VCC] - ConnectionVoltages[Connections.GND];
if (AccumulateOverstress(ref _stressSeconds, ref _stressTicks, vcc > 16.0, 0.001))
{
Fail("overvoltage on VCC (max 16V)");
return true;
}
return false;
}
public override void PowerCycle() { base.PowerCycle(); _stressSeconds = 0; _stressTicks = 0; }

The stress is accumulated in SIMULATED seconds, so the same abuse takes the same simulated time to kill the part at any tick rate — a tick-counted debounce fails 50x faster at 500 kHz than at 10 kHz, which is not physics. The helper drains while healthy, floors the trip at three consecutive faulted sweep calls (so one re-seeded spike can never latch), and EMPTIES the bucket at the latch, which is what lets the “Reset failures” action (it clears the latch WITHOUT power-cycling) hand the part a full fresh debounce. Clear both fields in PowerCycle for the designer’s Reset-state button, as above.

Mind the engine’s own short detection: a part drawing an absurd current from an IDEAL source races the source’s short-circuit latch (CircuitSimulation.ShortCircuitDebounceTicks), and whichever latches first wins. Debounce for the transient you must survive, and give sources a realistic internal resistance so the current stays physical.

Returns True to latch a failure; false while the component is healthy.

virtual

public virtual void PostTick();

Once per tick, UNCONDITIONALLY — unlike CheckFailure, this runs on failed-solve ticks and when CircuitSimulation.FailureChecksEnabled is off (the proven-circuit perf escape hatch), immediately after the solve settles and before the failure sweep. This is the hook for work that MUST happen every tick regardless of engine mode — the canonical use is joining a concurrent quantum (the RP2040’s pipelined tick) so nothing the element spawned survives the tick boundary. Only overriders pay: the engine sweeps a per-Analyze index of them, so a bench of plain parts sweeps nothing. The engine ALSO invokes this at hard-quiesce points (Analyze top, fence park, failure halt) with QuiesceRequested set — an element running a MULTI-tick quantum may skip joining on routine sweeps but MUST fully stop when the flag is up.

virtual

public virtual int UartFrameOn(int connection);

The frame of a NAMED port, packed (see UartFraming.Pack). The default packs the four properties above for every port; a multi-port part whose ports run at two framings (an ATmega2560 has four) overrides this instead. A relay’s ports have NO frame of their own - unknown in every field - so a relay is never compared as a receiver and never stamps as a sender; the originator’s frame is what reaches the far end.

virtual

public virtual void OnUartByte(byte value);

One byte arrived on UartRxConnection. Called on the sender’s clock; keep it short and never block. Exceptions are swallowed so a faulty device cannot hang whatever is transmitting, nor rob the other listeners on a shared line of the same byte.

virtual

public virtual void OnUartByte(byte value, int connection);

One byte arrived, naming WHICH port heard it. Only worth overriding on a multi-port part; the default hands single-port devices the byte through OnUartByte exactly as before, so nothing already authored has to know this exists.

virtual

public virtual void OnPacket(long tag, byte[] payload, int connection);

A packet arrived on one of PacketBusConnections. The payload is this component’s own private copy, so it may be kept or mutated freely. Called on the sender’s clock; keep it short and never block. Exceptions are swallowed so one faulty node cannot mute the rest of the bus.

THREADING, stated because it is a sharp edge and not a new one. This runs on whatever thread the SENDER is on, which for a microcontroller is that board’s own pacer thread. So a component wired between TWO boards can have this called by board A while board B’s thread is inside its SPI or I2C callback touching the same fields. The same is already true of OnUartByte and of every bus callback on a part wired to two masters — it is a property of the engine, not of this primitive — but a packet bus makes two-board topologies the NORMAL case rather than an exotic one, so it is worth knowing. A sandboxed component cannot lock (the type whitelist has no synchronisation primitives), so the practical rule for an author is: keep the handler short, and prefer appending to a buffer the component drains on its own clock over mutating state another master reads mid-transaction.

virtual

public virtual void OnBusGroupsRebuilt();

The bus group table was rebuilt BETWEEN ticks rather than by an Analyze, so Reset - where every per-topology cache normally dies - never ran. Anything keyed on a bus group must be dropped here. Called only when the table actually moved, and the default does nothing, which is right for every part that caches no group.

NODE IDS DID NOT MOVE. Only the grouping did, so a cache keyed on a raw net id is still valid and only the group-keyed ones are stale.

AND THIS RUNS ON THE SIM THREAD WITH NO QUIESCE. Analyze hard-quiesces every element pacer before Reset and this deliberately does not, so an override must be stores a chip thread can safely read either side of - one reference swapped, or one flag lowered - and never a multi-field struct left half written.

virtual

public virtual void OnI2CStart(bool read);

Called when a master addresses this device (START + address match). read is true for a read transaction (master will call OnI2CRead), false for a write (master will call OnI2CWrite). Typical use: reset your register-pointer state.

virtual

public virtual bool OnI2CWrite(byte value);

Called for each byte the master writes to this device. Return true to ACK (accept the byte) or false to NACK. A register-map device usually treats the first byte after a write-START as the register pointer and subsequent bytes as data.

virtual

public virtual byte OnI2CRead(bool last);

Called for each byte the master reads from this device. last is true when the master will NACK this byte (it is the final byte of the read). Return the byte to send — a register-map device returns the current register and advances its pointer.

virtual

public virtual void OnI2CStop();

Called when the master issues a STOP ending the transaction with this device.

virtual

public virtual bool IsBusPowerLead(int connectionIdx);

Declare a connection as a POWER pin (a regulated rail output, a supply pin). Bus routing’s resistive reachability drops any small-resistor link that touches a power net — a resistor into a rail is a pull-up or a load, never a series bus element. Voltage-source elements are detected automatically; servoed/conductance-based rails (a regulator model) must self-declare here.

virtual

public virtual byte OnSpiTransfer(byte mosi);

Called for each 8-bit SPI exchange while this device is selected: mosi is the master’s byte; return the byte to clock back on MISO. Full duplex, MSB-first unless the master set DORD (bit order is handled master-side; you receive whole bytes).

virtual

public virtual void OnConnectionEdge(bool high, double micros, double deltaMicros);

A line edge on the declared pin, mid-execution on the sim thread. high is the new resolved line level: the master’s drive while it drives, low while this device holds, and otherwise what the circuit makes of the line let go, so a line with no pull-up is low (EdgeSensitiveConnection says how and when that is measured). A pull-up is not assumed. micros is the master’s boot-relative time in microseconds — the same clock its firmware’s micros() reads; it restarts at zero whenever the MCU reboots, and an Analyze does not reboot it. deltaMicros is the width since the previous edge (the first edge after boot or after an Analyze reports a very large delta, so gap-latch logic like a NeoPixel’s >50 µs reset works unaided). Edges caused solely by this device’s own scheduled drives are not delivered, and neither is its own release when an Analyze or a reboot drops its hold. Exceptions are swallowed.

virtual

public virtual void OnConnectionEdge(int connectionIdx, bool high, double micros, double deltaMicros);

The same edge, tagged with the connection it arrived on — the form a device with more than one edge line must override, because on a two-wire part “the line went high” is only half the news. The bridge calls THIS one; the default forwards to the untagged OnConnectionEdge, so every single-line device that predates the plural declaration keeps working untouched.

abstract

public abstract void StampInitial();

StampInitial is called when the circuit Matrix is being initialized. This function should be used to stamp the matrix with the intial static unchanging values for a component.

virtual

public virtual void StampDynamic();

Optional, and nothing needs it: leave it out. It runs right after StampInitial each time the circuit is analysed. CRUMB prepares the matrix for the values Step changes by itself, and DeclareDynamicConductance in StampInitial covers any it can’t see. It stays so parts that already override it still compile.

abstract

public abstract void PreStep();

Runs once per tick, before the Newton loop. Latch this tick’s inputs here (UserData, ExternalSignal, sensed values, anything derived from TimeStep) and make every decision the tick depends on, so that Step, which runs on every solve pass, is a pure function of what was latched. Values published here for the other thread are always from a settled tick.

abstract

public abstract void Step();

Executes a single simulation step for the circuit logic. This method is responsible for updating the state of the circuit components based on the current simulation conditions. Derived classes must implement this method to define specific behaviors and interactions during each simulation iteration.

virtual

public virtual void GetMatrixPointers();

Called after Analyze() completes and the C++ CCS matrix is fully initialized. Non-linear components should override this to fetch direct pointers into the CCS Ax array using Simulation.GetConductanceMatrixPointer() and Simulation.GetRightSideIndex(). These pointers allow bypassing Insert_Value_CCS during Step() for maximum performance.

public double SampleEnvironment(string name, double fallback);

EXPLICITLY sample a named environment field at this component’s position — a component only reads the environment where its author asks to. Returns the value from the smallest enclosing environment zone that carries the field (name match is case-insensitive), or fallback when the component stands in no zone or no enclosing zone defines that name. Pass the matching property as the fallback so the inspector knob IS the bench value: SampleEnvironment("TemperatureC", Properties.TemperatureC).

public void SetFieldOutput(string sourceName, double value);

State what one of this element’s field sources is radiating, in the field’s own units: the value it would read on axis at the source’s reference distance.

Callable from Step, PreStep or CalculateCurrent, any number of times: only the LAST value recorded is committed, ONCE per tick, AFTER Newton convergence. That is not a convenience, it is the same discipline AccumulateDisplay follows - an unconverged pass reads trial voltages, and a diode’s exp() of those is nonsense that would be broadcast to every coupled receiver.

The commit point makes the coupling explicitly one tick delayed. At 100 kHz that is 10 us, 1.4 degrees at 400 Hz, and it is the correct answer rather than a compromise: two galvanically isolated circuits coupled implicitly would be one Newton system.

public double SampleReceiver(string receiverName, double fallback);

The named field arriving at one of this element’s receivers THIS TICK, through that receiver’s own shape: the ambient base plus every coupled source at its current drive.

Unlike SampleEnvironment, which reads a table the host refreshes per FRAME, this keeps up with the tick - which is the whole point, since a 400 Hz blip at 5 percent duty is under one hundredth of a frame and a per-frame table never sees it.

Returns fallback when no receiver of that name is bound, so the electrical half still works, and stays testable, on a part with no receiver at all. Pass the matching property, as SampleEnvironment does, and the inspector knob IS the bench value.

public void AccumulateDisplay(int slot, double value);

Record the instantaneous display value (LED brightness, lamp power, meter deflection…) for this tick. Callable from Step, PreStep or CalculateCurrent — any of them, any number of times: only the LAST recorded value is integrated, ONCE per tick, AFTER Newton convergence. (Step runs once per SOLVE PASS — an intermediate unconverged pass can momentarily read absurd voltages, and a diode’s exp() of those would poison a naive running sum beyond double precision. Recording + engine-side folding makes that impossible. CalculateCurrent runs once per tick on the tick’s last solve, so recording there is the safe default.) The frame side reads the exact time-average via GetDisplayAverage, so a 500Hz flasher renders as the steady half-glow a real eye sees instead of strobing with the frame rate. Up to 64 independent slots per component, one per segment or pixel of a display device; a slot past that is ignored.

Parameters

  • slot: Which display channel (0 to 63).
  • value: The instantaneous value.
public double GetDisplayAverage(int slot);

Frame-side read (ComponentLogic Update): the exact time-average of everything AccumulateDisplay recorded since this was last called — integration, not sampling, so no aliasing at any tick rate or frame rate. While the sim is paused it holds the last value (the world is frozen); after a reset/restart it returns 0 until new ticks arrive, matching how currents zero on reset.

Parameters

  • slot: Which display channel (0 to 63).
public byte[] AllocateFramebuffer(int sizeBytes);

Allocate (or fetch) this component’s pixel framebuffer — one flat byte array the circuit logic writes pixels into. Layout and pixel format are the component’s own contract with its ComponentLogic (an ST7735 stores RGB565 big-endian, row-major). Idempotent: repeated calls with the same size return the SAME array, so call it freely from StampInitial. After writing pixels call PublishFramebuffer to make the change visible frame-side.

Parameters

  • sizeBytes: Total buffer size, e.g. width × height × bytes-per-pixel.
public void PublishFramebuffer();

Release-publish the framebuffer: bumps FramebufferVersion so the frame side knows pixels changed. Cheap enough to call per decoded pixel — it is a single volatile increment.

public long ReadFramebuffer(byte[] dst);

Frame-side read (ComponentLogic Update): copy the framebuffer into dst (caller-owned, reused across frames) and return the version copied — 0 when no framebuffer exists. Check FramebufferVersion first and skip the copy while it hasn’t moved.

public byte[] AllocatePersistentImage(string propertyName, int sizeBytes);

Declare that propertyName - a byte-array property on this component - is a durable image, and allocate (or fetch) it at sizeBytes. Idempotent, so call it freely from StampInitial, which re-runs on every Analyze.

A component that never calls this has no durable image and costs the frame side one null check. Seed the returned array yourself from the authored property - allocation does not imply contents, exactly as with the framebuffer.

public void PublishPersistentImage();

Release-publish the durable image: bumps PersistentImageVersion so the frame side knows the bytes changed. A single volatile increment, so it is cheap enough to call per byte written.

public bool TryReadPersistentImage(byte[] dst, out long version);

Frame-side read: copy a CONSISTENT snapshot of the durable image into dst (caller-owned, reused). Returns false if the sim thread wrote during the copy, in which case dst is garbage and must be discarded - retry next frame.

⛔ THE CHECK IS THE API, which is why there is no plain copy overload to reach for. A version read before and after the copy that comes back equal means no writer touched the array in between. Handing out an unchecked copy and documenting that callers “should” bracket it produces a caller that does not, and the failure is a silently wrong saved image rather than an error - the worst shape a bug can have in something whose only job is remembering.

static

public static ulong StableSeedFrom(string identity);

A stable 64-bit seed from any durable identity string, for the host to pass to __SetIdentitySeed.

⛔ NOT string.GetHashCode(). .NET Core RANDOMISES string hashing per process by design, so using it here would have reintroduced the exact bug this whole region exists to fix - and it would have looked correct, because within any one run it agrees with itself. This is FNV-1a, which is fixed by its specification and will return the same number in ten years.

protected

protected byte[] IdentityMac(byte oui0, byte oui1, byte oui2, int stream = 0);

A uniform random number in [0,1) that is FIXED FOR THIS PLACED COMPONENT: the same on every call, the same after a save and reload, and different from every other placed component. A stable 48-bit hardware address for this placed component: the same on every call, the same after a save and reload, and different from every other placed component.

The three oui bytes are the vendor prefix the caller wants to present; the low three are drawn from this component’s identity. The LOCALLY-ADMINISTERED bit is not set: these addresses are meant to look like the real silicon’s, and a sketch printing its own MAC beside one on a sticker should not see a different shape.

⛔ WHY THIS IS NOT A COUNTER. Two boards on one air must not share an address - ESP-NOW addresses BY MAC, so a collision makes every unicast ambiguous - and a counter would give the same board a different address depending on what order the circuit happened to be built in, so a saved circuit would not reproduce and a paired peer list would rot on reload.

The agnostic primitive behind anything that should vary per unit but never drift: a resistor’s tolerance, a capacitor’s, an offset voltage, a noise seed, how tired one part is against another. It knows nothing about what any of those are.

Pass a different stream for each independent draw a part needs, so a capacitance deviation and an ESR deviation are not the same number wearing two hats.

Two placed copies of one component differ, because paste and duplicate re-mint the instance id. A part carried between circuits keeps its own deviation, because the id travels with it.

protected

protected double IdentityRandom(int stream = 0);

Not documented yet.

protected

protected double IdentityDeviation(double tolerance, int stream = 0);

A deviation in [1-tolerance, 1+tolerance], fixed for this placed component.

The shape every part that models manufacturing spread wants, so none of them has to carry its own copy of a mixer and get the stability question wrong independently. Multiply a nominal value by it. Tolerance 0 returns exactly 1.0, which is what a bench wants when it is comparing two instances and needs the reel held constant.

protected

protected int InternalNode(int i);

The matrix node id of private node i (0-based, see InternalNodeCount). Stamp against it exactly like a pin’s node.

protected

protected void InitialiseConnections();

Allocates the native lead buffers from TotalLeadCount (pins + private nodes). Called from the base constructor; protected so an element whose ConnectionCount is only known after its own constructor runs (e.g. a runtime-configured pin count) can re-run it once its fields are set. The base-constructor call with a zero count allocates nothing, so re-running never leaks.

public double GetConnectionVoltage(int connectionIdx);

Gets the current simulated voltage at the specified connection index.

public int GetConnectionNode(int connectionIdx);

Gets the assigned Matrix Node ID for the specified connection index.

public double GetBranchCurrent(int leadIdx);

The conventional current flowing INTO this element through lead leadIdx, in amps - computed BY THE ENGINE from an in-series LeadSenseElement, never by this element’s own code, so it is correct for any element regardless of what it stamps or publishes. Live only for leads the frontend has declared via CircuitSimulation.DeclareLeadSense while CircuitSimulation.StampWireCurrents is on (Current View); reads 0 otherwise. Tier-1: two atomic lead-voltage reads, safe from any thread at frame rate.

protectedonly in CheckFailure

[AllowedIn(nameof(CheckFailure))]
protected void Fail(string reason);

Records WHY the component is failing — call it from CheckFailure just before returning true, e.g. Fail("over-current: exceeded 7 A"); return true;. The message is shown to the user when the failure latches. Calling it and then returning false is harmless: the message is discarded, and FailureReason stays null while healthy.

Parameters

  • reason: Human-readable failure description.

protected

protected bool UartSend(byte value);

Transmit one byte from UartTxConnection. Returns false when nothing is wired to hear it — an unterminated line is worth reporting rather than assuming success.

protected

protected bool UartSend(int connection, byte value);

Transmit one byte from a NAMED port, for a multi-port part.

protected

protected int UartSend(byte[] bytes);

Transmit several bytes in order. Returns how many were delivered.

protected

protected int UartSend(int connection, byte[] bytes);

Transmit several bytes in order from a NAMED port. Returns how many were delivered.

protected

protected bool UartHasPeer();

Is anything wired to my TX line? Ask before transmitting to report an honest delivery failure rather than talking into an unconnected pin.

protected

protected bool UartHasPeer(int connection);

Is anything listening on a NAMED port’s line?

protected

protected int PacketSend(int connection, long tag, byte[] payload);

Put a packet on the net. Returns how many nodes heard it — 0 means nobody was listening, which is worth reporting rather than assuming success (on a real CAN bus a frame nobody acknowledges is an error, not a delivery).

protected

protected bool PacketHasPeer(int connection);

Is any other node on this net at all?

protected

protected void NotifyBusTransparencyChanged();

Say that this part’s BusTransparentConnections answer may have moved with its state. The engine re-reads every part’s declaration and rebuilds the bus groups at the next tick boundary: a table rebuild and NO Analyze - no element Reset, no node re-mint, no matrix refactorisation, and no reboot for any chip on the bench.

Call it from PreStep, where a part latches its position for the tick.

CALLING IT WHEN NOTHING MOVED IS GENUINELY FREE, and that is why the comparison is HERE rather than in the engine. This drains on the SIM tick - typically 100 kHz, not the 1 kHz physics tick - so a defensive author calling it every PreStep would otherwise buy a whole group-table rebuild a hundred thousand times a second. Compared against the pairs this part last published, which is an array of typically two ints, the call costs one virtual read and that compare. The engine still compares the TABLES afterwards, because two parts can move in ways that cancel and only the engine can see that.

AND WITHOUT IT THE GROUPS STAND UNTIL THE NEXT ANALYZE. A user closes the switch, the contact reads 0.1 ohm, every voltage on the bench is right, and the device behind it is still invisible to its master with nothing anywhere saying why.

protected

protected int FieldSend(byte[] to, byte[] payload);

Transmit on FieldChannel. Pass null as to to broadcast, or an address to unicast. Returns how many components received it — 0 means nobody was listening, which is worth reporting rather than assuming success.

protected

protected int FieldSend(byte[] to, byte[] payload, int kind);

As FieldSend, stamping the frame with a kind the receiver reads back as FieldFrame.Kind. A kind is a frame class both ends agree on; the engine assigns it no meaning.

protected

protected bool FieldHasListener(byte[] address);

Is anyone else listening at this address on this channel? Ask before transmitting to report an honest delivery failure. Null address = “is anyone on my channel at all”.

protected

protected FieldFrame FieldReceive();

Take the next frame waiting for this component, or null when there is none. You decide when to check — nothing is pushed into your logic on arrival.

public bool BusPinIsHigh(int connectionIdx);

The digital level of one of THIS device’s pins, safe to read inside a bus callback (OnSpiTransfer / OnI2C*): when the pin’s net is driven by the transacting master’s own GPIO, the LIVE mid-tick state is returned — firmware toggles control lines (an ST7735’s D/C) and transfers within one tick, so the converged node voltage lags. Otherwise it falls back to the converged electrical level (≥ 1.5 V reads high). Outside a transaction it is simply the converged level.

public void ScheduleConnectionDrive(int connectionIdx, double delayMicros, bool driveLow);

Schedule an open-drain transition of THIS device’s declared data pin, delayMicros microseconds from the master’s current instant (0 = now — legal only where the protocol makes the takeover unobservable, like a 1-Wire read-slot zero while the master still drives low). driveLow true pulls the line low; false lets it go, and it is then whatever the circuit makes of it: the line’s pull-up, the master’s pad pull, or this device’s own stamp to its supply. The master’s firmware sees the transition at the exact cycle via digitalRead/pulseIn/interrupts. Devices only ever pull LOW, which is what open-drain means; the high comes from the circuit. Queued transitions are cleared by any sim reset or re-Analyze. Calls while no MCU pin shares the net are ignored, like driving an unwired line.

public void ScheduleConnectionLevel(int connectionIdx, double delayMicros, bool high);

Schedule a PUSH-PULL transition of one of this device’s edge lines: drive it to a known level, high or low, delayMicros microseconds from the master’s instant (0 = now). The line then reads that level at once, with no circuit solve and no wait, the way a CMOS output stage drives a wire. Use it for any clocked serial output that changes level every bit: a data line shifted out on a master’s clock, a shift register’s serial output, a sensor’s echo or a receiver’s output pin. A master pad that is driving the same line still wins, as it does with an open-drain hold.

Called from inside OnConnectionEdge, the delay counts from the edge being delivered rather than from the master’s current instant, so a reply to a clock edge lands at the same point in every bit however far the master has run past the edge. Letting the line go is still ScheduleConnectionDrive with driveLow false: the line is then whatever the circuit makes of it, which a solve of the circuit decides. In Step, stamp the output stage that matches the drive, a stiff path to the supply while ConnectionDriveIsHigh and to ground while ConnectionDriveIsLow, so scopes and other parts on the net see it too. Calls while no MCU pin shares the net are ignored, like driving an unwired line.

public bool ConnectionDriveIsLow(int connectionIdx);

Whether this device’s scheduled open-drain pull is CURRENTLY holding its data pin low — the authoritative mirror of what the edge bridge has applied (scheduled transitions included as they land). Use it in Step to stamp the electrical pull-down (~100 Ω to ground) so scopes and other elements on the net see the truth per tick.

public bool ConnectionDriveIsHigh(int connectionIdx);

Whether this device is CURRENTLY driving one of its edge lines high push-pull, as ScheduleConnectionLevel scheduled it: the mirror of what the edge bridge has applied, scheduled transitions included as they land. Use it in Step to stamp the output stage’s stiff path to the supply, beside ConnectionDriveIsLow for the path to ground. False while the line is let go or held low.

public void DeclareDynamicConductance(int connectionA, int connectionB);

Pre-declare a dynamic conductance between two of this device’s connections from StampInitial, so the FIRST StampConductanceDynamic between them never misses the structure cache. A miss forces a mid-tick re-Analyze, which resets every element — including any MCU, which silently reboots. In-app logic gets this automatically from the code rewriter; hand-written mirrors (and belt-and-braces authors) call it explicitly.

public int InternalLead(int i);

The lead index of private node i, for the APIs below that take a lead. Private nodes live in the lead slots past ConnectionCount, so a single lead index addresses both real pins and internal nodes.

public void DeclareDynamicVoltageSource(int vsIndex, int positiveLead, int negativeLead);

Pre-declare a voltage source whose VALUE moves every tick, from StampInitial. Leads are given POSITIVE FIRST: DriveVoltageSource then holds the positive lead that many volts above the negative one. A lead is a pin (0 .. ConnectionCount-1) or a private node (InternalLead).

This is the analyze-phase half of a driven source, and it is what lets the solver PRECONDITION the matrix. Declaring it here marks the source’s row as one whose right-hand side changes during Step, so the structure is built once at Analyze and every later write lands in a slot that already exists. Without it the first write has nowhere to go.

The classic use is a source with real output impedance, which is every regulator, reference and op-amp output stage:

public override uint VoltageSourceCount => 1;
public override uint InternalNodeCount => 1;
public override void StampInitial()
{
// ideal source on a private node, then resist out to the real pin
DeclareDynamicVoltageSource(0, InternalLead(0), Connections.GND);
StampResistanceBetween(InternalLead(0), Connections.OUT, 75.0);
}
public override void Step() => DriveVoltageSource(0, _volts);

COMPONENT-AGNOSTIC: a driven source is not an op-amp. It is equally a regulator, a voltage reference, a DAC output or a signal generator.

public void DriveVoltageSource(int vsIndex, double volts);

Set the volts a declared dynamic source is holding, from Step. The counterpart to SetCurrent, and the only thing a part needs to drive a source.

Call it EVERY pass for every source you declared, unconditionally. The right-hand side is rebuilt from scratch each pass, so a source you skip reads as zero volts rather than holding its last value.

Pair it with DeclareDynamicVoltageSource in StampInitial. Writing to a source that was never declared does nothing, because no row was marked as changing.

public void DeclareVoltageGain(int vsIndex, int positiveLead, int negativeLead, double gain);

Make a declared source’s voltage TRACK a difference between two other leads, with gain. The solver then enforces it as a constraint rather than the part guessing at it.

Pair it with DeclareDynamicVoltageSource on the same source, in StampInitial. Together they solve

V(source) = whatever DriveVoltageSource holds + gain * (V(positive) - V(negative))

which is the whole reason an amplifier in feedback settles EXACTLY, in one solve, instead of being integrated toward an answer and overshooting it. The classic use is an op-amp: declare the source on a private node, give it a gain of a hundred thousand from the two inputs, and the matrix works out the output that satisfies the feedback network simultaneously.

Saturation is then a RIGHT-HAND-SIDE job, not a matrix one: work out the voltage you actually want, and drive

DriveVoltageSource(vs, wanted - gain * measuredDifference)

so the constraint above lands on exactly that value. In the linear region the two terms cancel and the source holds zero, which is the ideal amplifier falling out for free.

COMPONENT-AGNOSTIC: this is a voltage-controlled voltage source. It is an op-amp, an error amplifier inside a regulator, a differential receiver, or a gain block.

public void SetVoltageGain(int vsIndex, double gain);

Change a declared gain’s COEFFICIENT for this solve pass.

This is what lets an amplifier stop being an amplifier. While the gain constraint is active the solver enforces it absolutely, so nothing the part computes can hold the output anywhere else: any limit it tries to apply is divided by the gain and disappears. Dropping the coefficient toward zero converts the source from “track the inputs” to “hold this voltage”, which is what makes clipping, slew limiting and a released output actually bind.

Call it every pass alongside DriveVoltageSource. The working matrix is rebuilt from its static state before each pass, so this never accumulates, and passing the declared gain back is a no-op.

COMPONENT-AGNOSTIC: it is a gain block that can be turned down. An op-amp saturating, a regulator dropping out of regulation, an amplifier being muted.

public void DeclareTransconductance(int index, int currentInLead, int currentOutLead, int controlPositiveLead, int controlNegativeLead);

Pre-declare a voltage-controlled current source whose transconductance CHANGES every solve pass, from StampInitial. SetTransconductance then sets it per pass.

Current of siemens * (V(controlPositiveLead) - V(controlNegativeLead)) flows INTO currentInLead and OUT of currentOutLead. A lead is a pin (0 .. ConnectionCount-1) or a private node (InternalLead).

This is the primitive every three-terminal amplifying device is built from. A current that depends on a voltage somewhere ELSE cannot be written as a conductance between two pins, which is all DeclareDynamicConductance can offer: a conductance is symmetric and its control IS its own pair. Linearise the device each pass, stamp the cross term here, the self term as a dynamic conductance and the remainder as a dynamic current source, and Newton converges on the real curve instead of chasing it.

public override uint TransconductanceCount => 1;
public override void StampInitial()
{
// drain current, controlled by the gate-source voltage
DeclareTransconductance(0, Connections.D, Connections.S, Connections.G, Connections.S);
DeclareDynamicConductance(Connections.D, Connections.S);
}
public override void Step()
{
SetTransconductance(0, gm);
StampConductanceDynamic(ConnectionNodes[Connections.D], ConnectionNodes[Connections.S], gds);
StampCurrentSourceDynamic(ConnectionNodes[Connections.D], ConnectionNodes[Connections.S], ieq);
}

COMPONENT-AGNOSTIC: a transconductance is not a transistor. It is equally a JFET, a valve, an OTA, a current mirror, or the input stage of an amplifier.

public void SetTransconductance(int index, double siemens);

Set a declared transconductance for THIS solve pass, from Step, in siemens.

Call it every pass for every transconductance you declared, unconditionally. The working matrix is restored from its static state before each pass, so a value written here applies to that pass alone and one you skip reads as zero rather than holding its last value.

public bool AccumulateOverstress(ref double stressSeconds, ref int stressTicks, bool faulted, double limitSeconds);

Accumulated overstress in SIMULATED seconds - the leaky bucket every failure debounce shares. While faulted holds, the bucket fills by one TimeStep per tick; while healthy it drains at the same rate, because stress that stops is stress that dissipates. Returns true, and empties the bucket, once limitSeconds of net abuse has accumulated over at least three ticks.

TIME-BASED ON PURPOSE. A tick-count debounce fails fifty times sooner at 500 kHz than at 10 kHz for the same fault, because a tick is however long the solver says it is; silicon dies on energy and temperature, which is seconds. The three-tick floor is the one part that stays tick-natured: a post-Analyze reseed produces one or two ticks of garbage at ANY rate, and at very slow rates a single tick can exceed the whole time limit on its own. COMPONENT-AGNOSTIC: an over-current debounce, a thermal budget and a breakdown timer are all this one bucket with different limits.

public void StampResistanceBetween(int leadA, int leadB, double ohms);

A fixed resistance between two LEADS, so a part can resist between a private node and a pin without touching matrix node ids. Same lead numbering as DeclareDynamicVoltageSource.

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