Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Currents

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 run npm run install-local. The app is unsigned — see Shipping the DMG for the first-launch step.


Contents


What it does

  • 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.

Running it

npm install
npm start

Requires 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

Using the app

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.

Parts library

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).

How the simulation works

The schematic is compiled to a netlist, then solved with Modified Nodal Analysis:

  1. 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.
  2. 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.
  3. Reactive parts use trapezoidal companion models — a capacitor becomes a conductance 2C/Δt in parallel with a history current source. ESR and DCR are folded into the companion by exact series reduction rather than needing extra nodes.
  4. 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.
  5. 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.
  6. 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.
  7. Every node leaks to ground through 1 TΩ (GSHUNT, SPICE's gmin convention). 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.
  8. Fallbacks. If Newton stalls, the solver escalates gmin and 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.

Accuracy and verification

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.

Project layout

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.

Shipping the DMG

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.app

Two 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.

About the icon

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.

To rename the app

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.

Notes and decisions

  • 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: false in main.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.

About

A visual circuit simulator for macOS — real MNA solver, real device models, real wire resistance.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages