Circuit logic
Circuit logic is the electrical model of your part. It builds its behaviour by stamping conductances, sources and currents into the circuit matrix through Simulation, the CircuitSimulation the part belongs to.
You type fields, hooks and helper methods, never a class. Everything on BaseCircuitLogic, such as Simulation, ConnectionNodes and SetCurrent, is yours to use by name. See What you write.
The smallest useful part is a fixed resistor between two pins, A and B, with a Resistance property. This is the whole of its circuit logic, as you type it:
public override void StampInitial(){ Simulation.StampResistor(ConnectionNodes[Connections.A], ConnectionNodes[Connections.B], Properties.Resistance);}
public override void PreStep() { }
public override void Step() { }
public override void CalculateCurrent(){ double v = ConnectionVoltages[Connections.A] - ConnectionVoltages[Connections.B]; SetCurrent(0, v / Properties.Resistance);}ConnectionNodes[i] is the matrix node a pin sits on, and ConnectionVoltages[i] is the voltage solved there. You pass nodes to the stamps and read voltages back.
StampInitial, PreStep and Step must always be there, even when they’re empty. The other hooks are optional.
How a tick runs
Section titled “How a tick runs”The simulator advances in ticks of Simulation.TimeStep seconds. Within a tick it may solve the circuit several times (Newton passes) until nonlinear parts agree. Each hook runs at a specific point, and getting this right is most of writing good circuit logic.
| Hook | When it runs | Use it for |
|---|---|---|
StampInitial |
Once, when the circuit is analysed, before the first tick | Fixed stamps. Declaring the dynamic rows and conductances you will use. Deriving constants from TimeStep. |
PreStep |
Once per tick, before the solve | Latch this tick’s inputs (UserData, ExternalSignal, sensed values) and make your decisions. |
Step |
Every solve pass inside the tick | Stamp dynamic conductances and current sources. Clear Simulation.HasConverged while your operating point is still moving. |
CalculateCurrent |
Once per tick, after the last solve | Work out currents from ConnectionVoltages and publish them with SetCurrent. Advance per-tick state, such as a capacitor’s charge. |
CheckFailure |
Once per tick, on a converged solution | Decide whether the part has been destroyed. |
Reset |
Every time the circuit is re-analysed (a wire moves, a value changes) | Clear your scratch values. Call base.Reset(). |
PowerCycle |
When power goes off and on, and on Reset State | Clear failure stress and anything a real part loses at switch-off. |
Declaring your topology
Section titled “Declaring your topology”- Pins. CRUMB generates
ConnectionCountandConnections.<Pin>from your Connections markers, so you never declare them.Connections.<Pin>gives each pin’s index. - Voltage sources. Override
VoltageSourceCountto add source rows, and passVoltageSources[i]toStampVoltageSource. A part with sources is also covered by the automatic short-circuit check. - Internal nodes. Override
InternalNodeCountand useInternalNode(i)for junctions that aren’t pins, such as the point between a cell and its internal resistance. - Continuity. Override
ConnectionsAreConnected(a, b)to say whether two pins are joined for DC (a winding is, a capacitor isn’t). This feeds the ground and continuity checks.
Stamping
Section titled “Stamping”All stamps go through Simulation. and take node numbers from ConnectionNodes or InternalNode.
Fixed, in StampInitial: StampResistor, StampConductance, StampVoltageSource, StampCurrentSource. Use StampVoltageSourceDynamic for a source whose value you will change later. Use StampRightSideDynamic on a node that will carry an injected current, and StampNonLinear on a row whose left side changes each step.
Changing, in Step: StampConductanceDynamic and StampCurrentSourceDynamic are the workhorses of switches, diodes, transistors and anything with memory. Call DeclareDynamicConductance for each pair in StampInitial so the first tick doesn’t trigger a re-analysis.
Controlled sources for op-amps and transistors: StampVoltageControlledVoltageSource, StampVoltageControlledCurrentSource and StampCurrentControlledCurrentSource.
Reading the simulation: Simulation.TimeStep (recompute anything derived from it in both StampInitial and PreStep), HasConverged, SubIterations and SimTimeSeconds.
Capacitors, inductors and windings
Section titled “Capacitors, inductors and windings”Anything with memory uses the same recipe, a backward-Euler companion model between its two real pins:
- In
StampInitial, mark both nodes withStampRightSideDynamic. - In
PreStep, work out the companion conductance fromTimeStep(C / dtfor a capacitor,1 / (R + L / dt)for a winding) and the history current from last tick’s state. - In
Step, stamp both withStampConductanceDynamicandStampCurrentSourceDynamic. - In
CalculateCurrent, compute this tick’s current and keep it for the nextPreStep.
The motor example is a complete winding built this way.
Currents, displays and pixels
Section titled “Currents, displays and pixels”- Currents. Publish a branch current with
SetCurrent(slot, amps)and read it withGetCurrent(slot). This is what the multimeter and the overcurrent checks see, even on a part with no voltage sources. - Displays. For LEDs, segments and dot matrices, record a brightness with
AccumulateDisplay(slot, value)from the simulation side and read the averaged value withGetDisplayAverage(slot)from component logic. The averaging gives persistence of vision without aliasing. There are 64 slots. - Pixel displays.
AllocateFramebuffer(bytes), write your pixels, thenPublishFramebuffer(). Component logic checksFramebufferVersionand copies the frame withReadFramebuffer.
Talking to the outside
Section titled “Talking to the outside”UserDatais 16 numbers shared with your component logic. It is the only bridge between the electrical and physical halves. Read your inputs inPreStep, and write outputs inPreSteporCalculateCurrent, never inStep.ExternalSignalis one number fed in from outside the simulation each tick, such as audio input or a function generator.SampleEnvironment(name, fallback)reads a named field at the part’s position, such as"Lux"or"TemperatureC", from environment zones and emitters. Pass the matching property as the fallback, so the inspector value is the bench value when nothing around says otherwise.- The wireless field. Parts on the same channel can hear each other through
FieldSend,FieldReceiveandFieldHasListener. Nothing in it is specific to any radio.
Failure
Section titled “Failure”A part can be destroyed. In CheckFailure, decide whether it has been abused for long enough, then call Fail(reason) and return true. CRUMB isolates a failed part and heals it on a power cycle.
Judge abuse over simulated time, not ticks, because tick length changes with load. AccumulateOverstress does this for you. As a guide, allow about 1 ms for electrical abuse such as overvoltage and about 10 ms for heating. Zero your stress counters in PowerCycle.
public override bool CheckFailure(){ double watts = _volts * _volts * _conductance; if (AccumulateOverstress(ref _stressSeconds, ref _stressTicks, watts > Properties.MaxPower, 0.010)) { Fail("over-power: exceeded the " + Properties.MaxPower + " W rating"); return true; } return false;}What a failure looks and sounds like isn’t code. Add smoke, sparks or a sound from the component editor’s Failure tab, and they play when the part fails.
Serial buses and fast edges
Section titled “Serial buses and fast edges”Parts can take part in UART, I²C and SPI without modelling every bit:
- UART: declare
UartRxConnectionandUartTxConnection, handleOnUartByte, send withUartSend. Framing defaults to 8N1. - I²C: return your address from
I2CAddress, declareI2CSdaConnectionandI2CSclConnection, and handleOnI2CStart,OnI2CWrite,OnI2CReadandOnI2CStop. - SPI: declare the
SpiCs,SpiSck,SpiMosiandSpiMisoconnections and handleOnSpiTransfer. - Single-wire timing protocols (addressable LEDs, one-wire sensors): declare
EdgeSensitiveConnectionand handleOnConnectionEdge.
The BaseCircuitLogic reference documents each of these in full.
Things to avoid
Section titled “Things to avoid”- Don’t keep state in
staticfields, and remember that fields reset on Reset State (see State, saving and Reset).
The whitelist
Section titled “The whitelist”Circuit logic is checked against a strict whitelist before it compiles. Every type and namespace your code names must be on it. Anything else is refused: the editor marks the line with an error, and a part that breaks the rules won’t load, whether it comes from a save file or the Workshop.
Circuit logic may use:
- Everything in the
Simulationnamespace:BaseCircuitLogic,CircuitSimulationand the types that go with them. bool,byte,sbyte,short,ushort,int,uint,long,ulong,char,float,double,decimal,stringandobject.IntPtrandUIntPtr.List<T>, nullable values such asdouble?, andSystem.Math.- Arrays of any of these.
- Types you declare yourself, such as a small
structthat holds one sample.
That is the whole list. Circuit logic has nothing for the physical world, by design: see the two rules.
The check covers type arguments too: List<double> is allowed, and a list of anything off the whitelist isn’t.
Common things that aren’t on it: Dictionary, HashSet, tuples, Random, MathF, Func and Action, exception types and LINQ.
A method can also be tied to one hook. Fail can only be called from CheckFailure, and the reference marks any method like it with an only in tag. Call it in the hook itself, not from a helper method of your own.
The exact lists, read from CRUMB’s source, are on The whitelist in the API reference, with every method tied to one hook.
| Error | What it means |
|---|---|
CRUMB001 |
You called a method outside the hook it belongs to, such as Fail outside CheckFailure. |
CRUMB002 |
Your code names a namespace that isn’t on the whitelist. |
CRUMB003 |
Your code names a type that isn’t on the whitelist. The message lists what is allowed. |

