Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Circuits — draw one, and it works

The circuit kit takes a netlist and gives you a schematic that draws itself, carries real current, and answers questions. The numbers are not painted on: every voltage, current and glow comes from a Modified Nodal Analysis solve of the circuit you typed, pre-simulated at build time so it scrubs and records exactly.

Three ideas:

  1. Type the circuit — circuit(id, center, netlist)
  2. Play it — run(id, seconds)
  3. Take something outcut(id, part) / reconnect(id, part)
canvas("16:9"); template("paper");

circuit(rc, (640, 360), `
  dc-voltage 0 4 0 0 v=5
  resistor   0 0 5 0 r=1k
  capacitor  5 0 5 4 c=1u
  wire       5 4 0 4
`);
probe(rc, (5, 0));
run(rc, 6);

That block draws a schematic one component at a time, starts the current the moment the loop closes, and reads the capacitor charging to 63.2% of the supply at exactly one time constant — because it solved an RC circuit.


The netlist

One component per line. Two grid points, then optional key=value parameters:

<type> <x1> <y1> <x2> <y2> [key=value …]
ground <x> <y>
# a comment

The topology comes from the geometry. Points that coincide — or are joined by a wire — are the same electrical node. There are no node numbers to write and none to get wrong:

circuit(divider, (640, 360), `
  dc-voltage 0 6 0 0 v=9
  resistor   0 0 4 0 r=3k     # these two share the point (4,0),
  resistor   4 0 4 6 r=1k     # so that IS the divider's tap
  wire       4 6 0 6
  ground     0 6
`);

Values take SI suffixes — 1k, 4.7u, 100n, 10m, 2M. Lowercase m is milli and M is mega (SPICE’s habit of writing mega as M is a trap, so it is not one here).

ground is a reference, not a component

The solver needs one node to measure everything else from. With no ground line the kit elects the first source’s negative terminal, so a single loop solves identically either way — the symbol adds nothing and a textbook figure of a cell and a lamp does not draw one. Write a ground when several sources or loops make it worth saying which node is zero, or when the schematic’s convention wants the symbol.

A three-pin part still takes two coordinates

Its extra pins are derived, and this is the one thing worth memorising:

partpins
transistors, opamp, gates, comparator, vco, ideal-switch, spdt-switchthird pin one step below the second coordinate
flip-flopstwo outputs, one and two steps below the second coordinate
relaythird below the second, fourth below the first
transmission-line, dependent sourcesoutput pair below both written points
circuit(amp, (640, 360), `
  bjt-npn 3 3 6 3     # base (3,3) · collector (6,3) · EMITTER (6,4)
  …
`);

If a pin ends up connected to nothing, the error names it and its coordinate rather than saying “the circuit will not settle”.


It draws itself

A netlist is already written in the order a teacher would draw the circuit, so that is the default: components arrive one at a time, traced on, and each prefix of the netlist is really solved.

So nothing flows while the loop is open — the charge dots do not move because there is nothing to move them — and the instant the closing component lands, current appears everywhere at once. A probe reads 0.00 mA and then the true value. Nobody animates that.

The sixth argument is the share of run spent drawing:

circuit(rc, (640, 360), `…`, 46, 1, 0.35);   // the default
circuit(rc, (640, 360), `…`, 46, 1, 0);      // already drawn at t = 0

Use build 0 when the first frame matters — a film’s opening shot, a thumbnail — because a self-drawing circuit is invisible until a run plays it. (The editor warns if you forget the run.)


Reading it: probes and scopes

probe puts a live number on the schematic. Point it at a grid point for that node’s voltage, or name a part for its current:

circuit(rc, (560, 340), `
  dc-voltage 0 4 0 0 v=5
  resistor   0 0 5 0 r=1k name=R1
  capacitor  5 0 5 4 c=1u
  wire       5 4 0 4
  ground     0 4
`);
probe(rc, (5, 0));          // volts at the capacitor's top
probe(rc, R1, (80, -20));   // amps through R1, label nudged clear
scope(rc, (5, 0), (1020, 300), 380, 150);
run(rc, 6);

A scope traces the pre-simulated waveform with a sweep line that keeps step with run, and labels both axes with their real extent. Its window belongs to the circuit it was declared against — after a cut, declare another one or use a probe, which does follow across.


The current you can see

Charge dots ride every branch. Their position is ∫I dt — accumulated charge — from the same branch currents the solver produced, so a branch carrying twice the current always moves twice as fast, and a dot and a probe can never disagree.

current is the animator’s dial. It changes nothing about the physics:

current(rc, 2);                          // twice the pace
current(rc, 1.4, diamond, crimson, 4);   // speed, shape, colour, size
  • speed multiplies every branch by the same factor, so the proportionality survives — 1 is the circuit’s own pace, 2 is a fast cut, 0.5 is one you can talk over.
  • shape is circle (default), square or diamond.
  • color takes any palette name or #rrggbb. The default is gold, the conventional-current colour; cyan is the usual choice for electron flow.

A branch with no current shows no dots at all, rather than a frozen row of them — a stalled queue of dots reads as current, which is the one misreading worth designing against.


A lamp glows because it is dissipating power

lamp and led carry a halo whose opacity is P / P_ref, with P = V·I from the same series a probe reads. Two lamps in series share one current, so they light equally; a 300 Ω lamp in series with a 100 Ω one comes out three times brighter, because at one current P = I²R.

The reference is fixed by the circuit as first built, so brightness is comparable across the schematic and a cut genuinely darkens it. The halo is an ordinary entity tagged {id}.glow:

color(fig.glow, orange);   // a warm glow reads as light on `paper`

Take something out, and mean it

cut removes a component from the circuit, not from the picture: what is left is re-analysed and re-solved from scratch. Break a series loop and the current stops everywhere, and a meter on another branch falls to zero on its own.

run(fig, 6);            // both lamps lit
cut(fig, L1, 0.8);      // one filament breaks
run(fig, 3);            // … and neither glows
reconnect(fig, L1);     // mend it
run(fig, 3);            // both back

Address the part by its netlist name=, or by position as c0, c1, … Each run after a change replays the circuit that now exists. A cut that leaves something unsolvable (cutting the only source) is refused, with the part named.

cut decides the physics; you decide the theatre. By default the component fades out with its label, every dot and glow goes with it, and each probe moves to what it now reads. If the story is a broken filament rather than a lamp taken out of its holder, bring the symbol back and mark the break:

cut(fig, L1, 1.0);
par {
  show(fig.L1, 0.4);      // the lamp is still there …
  show(break1, 0.35);     // … it just cannot carry anything
}

Everything is an ordinary entity

The kit adds no animation vocabulary. Every piece is a normal tagged entity, so the core verbs work:

addresswhat it is
{id}.c{k}the k-th component, in netlist order
{id}.<name>a component you gave name=X
{id}.partsevery component (stroke batch)
{id}.labelsthe value labels
{id}.nodes · {id}.n{node}junction dots
{id}.chargethe charge dots
{id}.glowlamp/LED halos
{id}.probes · {id}.scopesreadouts
{id}.<type>severy part of one type — {id}.resistors, {id}.lamps
framebox(ring, fig.L1, 10);   // ring the component you are talking about
par { zoom(1.7, 1.2); cam((820, 420), 1.2); }
pulse(fig.L2);
color(fig.resistors, indigo);
erase(fig.parts, 0.4);        // erase the STROKES …
fade(fig.labels, 0.3);        // … and fade what is not a stroke

That last pair is the one gotcha: erase/draw are stroke verbs, so on a bare circuit id they leave the labels and dots behind. Address {id}.parts and fade the rest.


What is in the kit

44 component types, from the 102-preset library the solver was ported from:

grouptypes
passive + sourceswire ground resistor capacitor polarized-cap inductor dc-voltage ac-voltage dc-current clock lamp
switchingswitch push-switch spdt-switch ideal-switch relay fuse
junctionsdiode led zener
transistorsbjt-npn bjt-pnp mosfet-n mosfet-p jfet-n jfet-p darlington-npn darlington-pnp
analogueopamp comparator schmitt vco transmission-line
combinational logicand-gate or-gate nand-gate nor-gate xor-gate not-gate logic-input logic-output
sequential logicd-flipflop sr-flipflop jk-flipflop

Junctions, transistors and op-amps are solved by Newton–Raphson with voltage limiting; capacitors and inductors use trapezoidal companion models; a Darlington really is two transistors sharing an internal node, and a transmission line really is a ladder of L–C sections, which is why a step arrives late.


The physics you can rely on

  • KCL and KVL hold by construction. Every node row of the matrix is a current balance and every voltage source adds a row that pins its branch, so any solution the kit accepts satisfies both to solver precision.
  • The timestep is the circuit’s, not the frame rate’s. It integrates at the smallest time constant the circuit demands and decimates the output down to playback. Coarsening the integration would change the answer; coarsening what is stored cannot.
  • It is pure in t. The whole transient is solved at build time, so scrubbing backwards — including back through a construction or a cut — is exact.
  • Closed-form checks, in the test suite. Ohm’s law, series/parallel, a divider’s tap, an RC step at 63.2% of one time constant, RL rise, series-RLC resonance at 1/(2π√(LC)), a diode’s forward drop, a half-wave rectifier’s floor, β² gain on a Darlington, a 1 µs delay line arriving at 1 µs.

End to end: a class-9 question

“If the filament of one of the lamps is broken, will the other glow? Justify your answer.” The answer is nowhere in this file — it comes out of the solver.

canvas("16:9"); template("paper");

circuit(fig, (560, 330), `
  lamp       1 0 5 0 r=120 name=L1
  lamp       5 0 9 0 r=120 name=L2
  wire       9 0 9 4
  wire       9 4 6 4
  dc-voltage 6 4 4 4 v=6
  wire       4 4 1 4
  wire       1 4 1 0
  ground     6 4
`, 62, 0, 0);

current(fig, 1.4, circle, crimson, 4);
color(fig.glow, orange);
probe(fig, L1, (-14, -52));

run(fig, 6);          // one current, so both lamps light equally
cut(fig, L1, 1.0);    // break one filament
run(fig, 4);          // both dark, and the meter reads 0.00 mA
reconnect(fig, L1, 1.0);
run(fig, 4);          // mended

A series circuit is one path. Break it anywhere and you have broken it everywhere — and here that sentence is a measurement, not a claim.

See the circuits gallery for the full scenes.