A visual circuit simulator for macOS. Draw a schematic on a grid, press Run, and watch real physics happen — charge moving through wires, capacitors filling, LEDs lighting at the brightness their forward current actually implies.
The point of difference is that it doesn't fake anything. Under the canvas is a proper Modified Nodal Analysis solver with trapezoidal companion models for reactive parts and damped Newton-Raphson for semiconductors — the same approach SPICE uses. Wires have real resistance based on their AWG gauge and run length.
Status: shipped, v0.3.0.
dist/Currents-0.3.0-arm64.dmg(and the x64 build) are ready to install, or runnpm run install-local. The app is unsigned — see Shipping the DMG for the first-launch step.
- What it does
- Running it
- Using the app
- Parts library
- How the simulation works
- Accuracy and verification
- Project layout
- Shipping the DMG
- Notes and decisions
- Schematic editor — grid snapping, rotate/flip, multi-select, rubber-band select, undo/redo, copy/paste, save/open, PNG export.
- Live simulation — runs continuously at a selectable time scale from 1× down to 0.0005× so you can watch a 60 Hz waveform or a microsecond edge.
- Real wire behaviour — pick a gauge from 4/0 to AWG 30 and a run length, and the wire's resistance, voltage drop, dissipation and current density are all in the answer. Push past its ampacity and it glows.
- Visual overlays — node voltage colouring, animated current flow whose dot density scales with magnitude, and component value tags.
- Oscilloscope — drop Scope Probes, Voltmeters or Ammeters and their traces are captured to a rolling CRT-style display with auto-scaling.
- Live measurement panel — select anything and get its voltage, current, power, plus part-specific readings: a resistor's colour code and rating headroom, a capacitor's stored charge and energy, a BJT's operating region and β, an LED's forward current as a percentage of its rating.
npm install
npm startRequires Node 18+ and macOS. Everything else is vendored by npm.
Useful scripts:
| command | what it does |
|---|---|
npm start |
run the app |
npm test |
headless engine checks against closed-form answers |
npm run shots |
render screenshots of every example to build/shots/ |
npm run icons |
regenerate the pixel-art icon candidates |
npm run icns |
build build/icon.icns from a chosen candidate |
npm run dist |
build the .dmgs (staging folders are cleaned up afterwards) |
npm run install-local |
install the newest arm64 build into /Applications |
Placing parts. Click a part in the left palette, then click on the grid. Hold Shift while placing to keep placing more of the same. Press R to rotate (works on the ghost preview too), Esc to cancel.
Wiring. Press W or hit the Wire button, then either:
- click once to start, click each corner to extend, and finish with Esc, Enter, a double-click or a right-click — the best way to draw a run with several bends; or
- press-drag-release for a single elbow, which finishes on its own.
The anchor the run is growing from is marked on the canvas and the next segment previews from it. Diagonal runs auto-route with an elbow; hold Shift to flip which leg comes first. Wires connect at endpoints and where an endpoint lands on another wire's run (a T-junction) — wires that merely cross do not connect, same as a real schematic.
Interaction. Click a switch to toggle it. Hold a push button to press it. Drag a potentiometer's slider in the inspector while the sim runs.
Editing several parts at once. Select any number of things and the inspector stays editable — the edit applies to the whole selection. Rubber-band a bundle of wires and one click on a colour swatch recolours all of them, or change their gauge, run length or temperature together. Any property where the selected items don't already agree is tagged mixed, so you can see what you are about to flatten. A selection of parts that are all the same type works the same way; in a mixed selection you still get the wire fields, applied to just the wires.
Overlays. The toolbar toggles at the top right are node voltage colouring (⌘1), current flow animation (⌘2), component values (⌘3), the oscilloscope (⌘4) and heat (⌘5).
Heat view haloes every part by how hard it is being pushed relative to its own rating — dissipation against the package rating for resistors and semiconductors, current against ampacity for wires, forward current against the rated maximum for LEDs, current against the rating for fuses. Blue is cool, amber is at the rating, red is over it. The inspector also shows "Thermal load" and an estimated temperature rise for any part that has a rating.
Keys. Space run/pause · V select · W wire · R rotate · F flip · ⌘1–5 toggle overlays · ⌘0 zoom to fit · ⌘Z undo.
Single-key shortcuts are handled by the window, not by menu accelerators, so they stay inert while you are typing in a field. (Menu items still show the key; they just don't register it — see Notes and decisions.)
Value entry accepts engineering notation the way you'd actually write it:
4k7, 4.7k, 10meg, 2u2, 100n, 0.22. Resistors snap to E24 preferred
values by default (toggle at the bottom of the palette).
Examples menu has nine worked circuits: voltage divider, RC low-pass, LED with current-limiting resistor, RLC tank, half-wave rectifier with smoothing, BJT common-emitter amplifier, a wire-gauge voltage-drop demo, a 7400 quad NAND gate driven from two switches, and a 1 kHz crystal oscillator clock.
| group | parts |
|---|---|
| Sources | Battery (7 real cell types with internal resistance), DC supply, AC/function generator (sine, square, triangle, sawtooth, pulse), current source, ground |
| Passives | Resistor (with colour bands and tempco), capacitor (ceramic/film/electrolytic/tantalum with ESR and leakage), inductor (with DCR and saturation), potentiometer (linear/log taper) |
| Wiring | Wire (AWG 4/0–30, copper/aluminium/silver, 10 insulation colours), fuse (fast/slow-blow) |
| Switches | SPST switch, push button (NO/NC) |
| Semiconductors | Diode (1N4148, 1N4001, 1N5817 Schottky, 1N4733A/1N4740A Zeners), LED (7 colours with correct Vf and wavelength), BJT (2N3904, 2N2222, BC547, 2N3906, BC557) |
| Output | Incandescent lamp (with tungsten thermal model), buzzer, DC motor |
| Clocks | Quartz crystal (Butterworth-Van Dyke model, 32.768 kHz to 20 MHz), crystal oscillator can (XO) with VCC/GND/EN/OUT and real logic-level output |
| Logic | 14-pin DIP chips with real pinouts: 7400 quad NAND, 7408 quad AND, 7432 quad OR, 7486 quad XOR, 7402 quad NOR, 7404 hex inverter — each with a selectable 74HC / 74LS / CD4000 family |
| Meters | Voltmeter, ammeter, scope probe, text note |
Component models are real. LED saturation currents are back-solved from the datasheet forward voltage at 20 mA, so a red LED sits at ~2.0 V and a blue one at ~3.2 V without anyone hand-waving. BJTs use published Gummel-Poon parameters (the 2N3904's BF is 416.4, not a round 100).
The schematic is compiled to a netlist, then solved with Modified Nodal Analysis:
- Node extraction. Every component pin and wire endpoint becomes a point. Wires are split at every point that lands on them, so a mid-run tap gets its own node and its proportional share of the wire's resistance. Ground symbols are merged and become node 0.
- Stamping. Each element contributes conductances to the matrix and currents to the right-hand side. Voltage sources add an augmented row with their current as an extra unknown.
- Reactive parts use trapezoidal companion models — a capacitor becomes a
conductance
2C/Δtin parallel with a history current source. ESR and DCR are folded into the companion by exact series reduction rather than needing extra nodes. - Nonlinear parts are solved by Newton-Raphson. Each iteration linearises
the junction to a conductance plus an equivalent current. Two details matter
a lot here and are easy to get wrong:
- Junction limiting. Raw Newton steps on an exponential will overflow
immediately, so junction voltages are damped logarithmically (SPICE's
pnjlim), and the same damping is applied to(-V - BV)so reverse breakdown converges too — that's what makes Zeners work. - Convergence must account for limiting. If a junction is still being limited, the solution vector can look settled while the device is nowhere near its true operating point. The solver refuses to declare convergence while any limiting is active. Without this, transistors silently sit at cut-off and everything downstream reads plausible but wrong.
- Junction limiting. Raw Newton steps on an exponential will overflow
immediately, so junction voltages are damped logarithmically (SPICE's
- Logic gates sit outside the Newton loop on purpose. Each gate's output is a totem pole — one switched resistance to the chip's VCC pin, one to its GND pin, only ever one conducting — so the load current is really drawn through the chip's supply pin and fan-out behaves. The logic itself is re-evaluated between solves from the previous node voltages, which keeps every solve linear; the cost is one timestep of propagation delay per gate, which is realistic anyway. Inputs latch with hysteresis so a signal sitting in the threshold band doesn't chatter, and a chip with no supply goes high-Z rather than driving.
- Clock sources come in two forms. A quartz crystal is the full Butterworth-Van Dyke equivalent — a motional arm (Lm-Cm-Rm) across the holder capacitance C0 — with Lm derived from the rated frequency and Cm, so a 32.768 kHz watch crystal really does come out at ~7.9 kH and Q ≈ 54,000. An oscillator can (XO) reuses the logic totem-pole output, so its clock is a proper logic-level square wave that sources real current from its VCC pin and tri-states when EN is pulled low. Both feed the automatic timestep, which is why the floor drops to 0.1 ns — an MHz part still gets ~500 points per cycle.
- Every node leaks to ground through 1 TΩ (
GSHUNT, SPICE'sgminconvention). Without it, a section with no DC path to ground gives an all-zero matrix row, the factorisation reports singular, and the whole circuit collapses to 0 V — so dropping an unconnected part on the sheet would stop the entire simulation. 1 TΩ is far too weak to disturb real readings. - Fallbacks. If Newton stalls, the solver escalates
gminand retries (gmin stepping), then reports honestly rather than showing garbage.
The timestep is chosen automatically from the fastest source in the circuit (Δt ≈ 1/500f, clamped to 20 ns … 10 µs). Solving is dense LU with partial pivoting, which is plenty for the matrix sizes an interactive schematic reaches.
npm test runs 67 checks against closed-form answers. They're not smoke tests —
several of them caught real bugs during development, including two in the logic
chips after they were written:
── Reference data ─────────────────────────────────────────
ok AWG 22 copper, 1 m @ 20 °C 0.052960 Ω
ok AWG 22 copper, 1 m @ 75 °C (tempco applied)
── Ohm's law / dividers ───────────────────────────────────
ok divider 9 V · 10k/4k7 midpoint 2.87755 V
ok parallel 1k||2k||4k total current
ok 9 V cell (1.7 Ω) terminal V into 10 Ω 7.69231 V
── Reactive ───────────────────────────────────────────────
ok RC step at t = τ 63.2 % of final
ok RL step current at t = τ
ok LC tank frequency (1 mH, 1 µF) 5032.9 Hz
── Nonlinear ──────────────────────────────────────────────
ok 1N4148 Vf near 1 mA
ok Vf rise for 2× current ≈ n·Vt·ln2 (ideality factor)
ok red LED forward voltage / current 1.93 V, 15.1 mA
ok 1N4733A shunt regulation 5.04 V
ok CE amp: Ie ≈ Ve/Re, Ic = Ie − Ib, Vc = 12 − Ic·Rc
ok CE amp: beta = BF / (1 − Vbc/VAF) (Early effect)
── Kirchhoff ──────────────────────────────────────────────
ok Wheatstone bridge nodes vs hand-solved node equations
ok KCL residual at node A 4e-12
── Floating sections ──────────────────────────────────────
ok divider still solves with junk on the sheet
ok solver did not report singular / did converge
── Wire segments ──────────────────────────────────────────
ok rail split at a mid-run tap carries 20 mA then 10 mA
── Logic chips ────────────────────────────────────────────
ok full truth tables for 7400/7402/7404/7408/7432/7486
ok 74LS high level ≈ VCC − 1.6, CD4000 at 12 V → 12 V
ok unpowered chip output stays high-Z
ok gate drives 5 mA into 1k, drawn from the supply
ok chained inverters settle back to the input level
── Crystals and clocks ────────────────────────────────────
ok watch crystal Lm ≈ 7.9 kH, Q ≈ 54,000
ok 32.768 kHz crystal rings at fs within 1 Hz
ok 1 MHz crystal rings at fs within 30 Hz
ok XO frequency, duty, output level, supply sourcing
ok EN low tri-states the XO output
Wire data is solid copper at 20 °C (ρ = 1.724e-8 Ω·m) with the standard chassis-wiring and power-transmission ampacity columns, and a 0.00393/°C temperature coefficient.
main.js Electron main process, menus, file dialogs
preload.js context-isolated IPC bridge
app/
index.html UI shell
css/style.css dark instrument-panel styling
js/util.js LU solver, engineering notation parse/format
js/parts.js component catalogue + AWG / colour-code / device models
js/engine.js MNA netlist builder and simulator
js/render.js canvas symbols, overlays, current animation
js/scope.js rolling oscilloscope
js/examples.js the seven worked circuits
js/app.js document state, interaction, inspector
tools/
test-engine.js headless verification suite
shoot.js screenshot harness
make_icons.py pixel-art icon generator
make_icns.sh PNG -> .icns
build/
icons/ icon candidates
shots/ rendered screenshots
Scripts are plain <script> tags rather than ES modules, because Chromium
blocks module loading from file:// and Electron's loadFile serves from
exactly that. No bundler, no build step for the renderer.
python3 tools/make_icons.py --iconset 03-breadboard build/icon.iconset
iconutil -c icns build/icon.iconset -o build/icon.icns
npm run dist # -> dist/Currents-<version>-{arm64,x64}.dmg
npm run install-local # -> /Applications/Currents.appTwo DMGs are produced, arm64 and x64, each with a drag-to-Applications window.
What ends up in dist/. Only the DMGs and their blockmaps. While building,
electron-builder also stages an unpacked Currents.app per architecture in
dist/mac-arm64/ and dist/mac/ — those are the bundles it wraps into the
DMGs, not stray copies, but together they are about 470 MB of duplicated
Electron runtime. A postdist script (which npm runs automatically after
npm run dist) deletes both staging folders once the DMGs exist, so they don't
accumulate. Nothing is lost: they are regenerated on the next build.
Installing locally. npm run install-local runs tools/install-local.sh,
which mounts the newest arm64 DMG, stages the app beside the target so a failed
copy can't leave /Applications empty, quits any running copy, swaps it in and
clears the quarantine flag. It installs from the DMG precisely because the
staging folders are cleaned up after each build.
First launch. The build is unsigned (identity: null in package.json), so
Gatekeeper will block a DMG you double-click. Either right-click the app →
Open → Open, or:
xattr -dr com.apple.quarantine /Applications/Currents.app(install-local already does this for you.)
Signing and notarising needs a paid Apple Developer ID. Once you have one, set
identity to your certificate name and add a notarize block.
The icon ships two artworks in one .icns. The detailed 48×48 breadboard is
used from 128 px up; below that its hole grid aliases into grey mush, so a
simplified 16×16 variant takes over for 16/32/64 px — full-bleed board, bolder
rails, one banded resistor and one lit LED, scaled by integer factors so it
stays crisp. tools/make_icons.py --iconset <key> <dir> handles the switching;
SMALL_VARIANTS maps which icons have a small version.
To change the icon later, pick another key from build/icons/ (01-resistor,
02-led, 03-breadboard, 04-loop, 05-scope, 06-pcb) and re-run the three
commands above.
The name lives in four places: name, productName and build.productName in
package.json, plus the <title> and .app-name span in app/index.html.
The project folder is still called Circuits/, which is cosmetic only —
nothing reads it.
- Wire resistance is on by default. It's the thing that makes the simulator feel real — you can watch a 55 W lamp on 10 m of AWG 18 pull the rail down to 6 V. There's an "Ideal wires" toggle in the palette for when it's just noise.
- Live V/I tags only show for the hovered or selected part. Showing them on everything turned a schematic into a wall of numbers.
- Reactive state survives edits. Capacitor charge and inductor current are carried across netlist rebuilds, so tweaking a resistor mid-run doesn't discharge the circuit.
- No AC/frequency-domain analysis yet. Everything is time-domain transient. A proper AC sweep with a Bode plot would be the obvious next addition.
- The DC motor is electrically modelled but not mechanically — winding resistance and inductance are real, back-EMF and inertia are not.
- Menu items show single-key shortcuts but don't register them
(
registerAccelerator: falseinmain.js). An Electron accelerator fires ahead of the page and regardless of focus, so having R, F, Space and Backspace in both the menu and the renderer meant every press fired twice — rotate went 180° and skipped the vertical orientations, flip cancelled itself out, pause did nothing — and Backspace couldn't be typed into a text field. The renderer owns these keys now, which also gets the focus behaviour right for free. - The inspector is only rebuilt on selection changes. It used to be rebuilt on every netlist rebuild, which replaced the DOM node you were dragging and so cancelled slider drags after a single step. Live values are refreshed in place instead.
- Logic gates are evaluated between solves, not inside Newton. Discontinuous switching inside the Newton loop is a classic non-convergence source. Sampling the inputs once per timestep keeps every solve linear and costs one timestep of gate delay, which is a fair model of propagation delay rather than a fudge.
- Gate outputs are switched resistances to the real supply pins, not ideal sources referred to ground. The first version used ideal sources and the load current appeared from nowhere — the supply read 10 nA while an LED drew 12 mA. Modelling the totem pole properly makes supply current, fan-out and unpowered chips all behave.
- Use the XO for a clock, not a crystal and an inverter. The crystal model itself is accurate — it rings at its rated frequency to within a part per million, and its Lm/Q/pull figures match real datasheets. But building a Pierce oscillator out of a crystal and a 7404 does not give a crystal-controlled clock in this simulator, and it is worth knowing why: a real Pierce circuit biases the inverter into its linear region with the 1 MΩ feedback resistor and uses it as a high-gain analogue amplifier. The logic-gate model here is a hard-switching comparator with hysteresis and has no linear region at all, so the loop instead free-runs as an RC relaxation oscillator on the bias resistor and load capacitors. Measured: it oscillates at ~28 kHz with a 32.768 kHz crystal, and the motional current stays in the nanoamps — the crystal is barely participating. The crystal is therefore useful as a resonator (tanks, filters, ringing) and the XO is the right part for clocking a digital circuit.
- Current-flow dots are scaled against the largest current in the circuit, not an absolute log scale. On an absolute scale a parallel branch carrying a tenth of the main current crawled so slowly it read as carrying nothing, which is what made it look like electricity wasn't taking all paths. The physics was always right; only the animation was lying.