Skip to main content
OmniSpice screenshot

Co-Founder & Design Engineer · Umbra Labs · 2026 - Present

OmniSpice

Circuit simulation for students stuck between LTspice's 1990s interface and lab machines they can't install anything on.

ngspice 45

Engine

60ms

Live Feedback

70 + 29

Test Suite

Pre-launch

Status

React 19TypeScriptngspice (WASM)React FlowuPlotYjsHonoCloudflare Workers

The problem every circuits student knows

The solver is fine. ngspice descends from the same Berkeley SPICE that industry has trusted since the 1980s, and it still gets the right answers. Everything wrapped around it is the problem: installers that need admin rights, schematic editors that fight you, error messages written for the people who built the simulator. Universities standardize on LTspice because it is free. Students put up with it because nobody has given them a choice.

  • Needs a local install and admin rights. Locked-down lab machines and student Chromebooks are both out
  • The interface predates most of its users. Modal tools, cryptic hotkeys, right-click everything
  • Errors come straight from the solver. "Singular matrix" tells a sophomore nothing about the floating node in their schematic
  • Zero connection to the LMS. Students screenshot waveforms into Word docs and TAs grade the PDFs by eye
  • No collaboration. Lab partners crowd around one machine and take turns driving

How it's built

The whole simulator runs client-side. ngspice 45 compiled to WebAssembly does the actual math in a Web Worker, so the numbers come from the same solver a professional would use, with nothing to install. Around that core, every design decision starts from how students actually work: live feedback while editing, errors in plain English, and results that reach the gradebook without a screenshot in sight.

ngspice in a Web Worker

ngspice 45 compiled with Emscripten, running single-threaded over a pipe-style stdin/stdout interface. No SharedArrayBuffer, deliberately: the COOP/COEP headers it requires would break embedding inside LMS iframes, and grade passback only matters if the app can live inside Canvas.

Tiered live simulation

Four lanes with different latency budgets. DC operating point re-runs on every edit. AC sweeps debounce at 60ms with a 500ms cap so continuous knob-scrubbing still produces fresh curves. Transients preview against the last committed run and commit on release. Sweep points cache by netlist hash.

Errors a sophomore can read

A translation layer turns raw solver output into messages that name the actual node and suggest a fix. Floating-node detection instead of "singular matrix".

LTspice .asc import

A parser and component mapper for LTspice schematic files, so a course's existing materials carry over without redrawing every circuit.

Debounce with a starvation guard

Dragging a parameter knob can fire hundreds of AC sweep requests per second. A plain debounce would wait for the pointer to stop, which means no feedback during exactly the interaction that needs it. This lane coalesces rapid edits into one run through a 60ms sliding window, and a 500ms max-deferral fires the sweep anyway if the scrubbing never pauses.

View Code
From src/simulation/TieredSimulationController.ts. Every caller gets a promise; when the coalesced run finishes, all waiters in the window resolve from the same result.
typescript
scheduleAcSweep(netlist: string, params: AcParams): Promise<VectorData[]> {
  return new Promise<VectorData[]>((resolve, reject) => {
    this.acPendingWaiters.push({ resolve, reject });
    this.acPendingArgs = { netlist, params };

    const now = Date.now();
    if (this.acFirstScheduleTime === null) {
      this.acFirstScheduleTime = now;
    }

    if (this.acDebounceTimer !== null) {
      clearTimeout(this.acDebounceTimer);
    }

    // Sliding 60ms debounce, capped by a 500ms max-deferral so
    // continuous scrubbing can't starve the sweep forever.
    const elapsed = now - this.acFirstScheduleTime;
    const remaining = TieredSimulationController.AC_MAX_DEFERRAL_MS - elapsed;
    const delay = Math.max(
      0,
      Math.min(TieredSimulationController.AC_DEBOUNCE_MS, remaining),
    );

    this.acDebounceTimer = setTimeout(() => {
      void this.flushAcDebounce();
    }, delay);
  });
}

Architecture

A React SPA where the heavy machinery lives client-side, backed by a Hono Worker for everything a classroom needs to share.

Architecture Layers

Schematic editor

React Flow canvas with a 24-component library, pin-type compatibility checking during wire drag, a command palette, and inline parameter chips with scrub gestures.

React 19React FlowZustandcmdk

Simulation engine

ngspice 45 compiled to WebAssembly, isolated in a Web Worker. Results cached by circuit hash on both sides of the message channel so unchanged netlists never re-run.

ngspice 45EmscriptenWeb Worker

Waveform viewer

uPlot on oscilloscope duty: zoom, pan, cursor readouts, Bode plots. Chosen for raw draw speed at 20KB gzipped.

uPlot

Collaboration

Yjs CRDTs synced through Cloudflare Durable Objects over hibernatable WebSockets, with y-indexeddb persistence so circuits survive offline.

Yjsy-durableobjectsy-indexeddb

Classroom backend

Hono on Cloudflare Workers. D1 holds courses, assignments, and LTI state; R2 holds circuit blobs and reference waveforms; a cron-driven queue retries dropped grade submissions.

HonoD1R2Clerk

LTI 1.3, self-hosted on Web Crypto

The standard library for LTI, ltijs, pulls in Express and Mongoose. Neither runs on Cloudflare Workers, so the whole flow is built directly on jose and the Web Crypto API: OIDC login, deep linking, a JWKS endpoint, grade passback with retries. Launch verification is a single pipeline with the I/O injected, which keeps the unit tests hermetic. One extra trick: launches mint a Clerk sign-in ticket server-side, so students land authenticated inside the LMS iframe without fighting third-party cookie blocking.

View Code
Condensed from worker/src/lti/verify.ts. Platform lookup, JWKS fetch, and the nonce store are injected, so tests run without D1 or network access.
typescript
export async function verifyLaunch(
  idToken: string,
  options: VerifyLaunchOptions,
): Promise<LtiLaunchPayload> {
  // 1. Peek at iss + aud without verifying
  const peek = decodeJwtPayload(idToken);
  const peekIss = typeof peek.iss === 'string' ? peek.iss : '';
  const peekAud = pickAud(peek.aud);

  // 2. Platform must be registered for (iss, client_id)
  const platform = await options.platformLookup(peekIss, peekAud);
  if (!platform) {
    throw new Error('Unknown platform: ' + peekIss);
  }

  // 3. Fetch the platform JWKS, pick the key matching kid
  const jwks = await options.fetchJwks(platform.jwks_uri);
  const kid = decodeJwtHeader(idToken).kid;
  const jwk =
    (kid ? jwks.keys.find((k) => k.kid === kid) : undefined) ?? jwks.keys[0];
  const publicKey = await importJWK(jwk, jwk.alg ?? 'RS256');

  // 4. Signature, expiry, issuer, audience
  const { payload } = await jwtVerify(idToken, publicKey, {
    issuer: platform.iss,
    audience: platform.client_id,
  });

  // 5. Typed claim validation
  const claims = LtiLaunchClaimsSchema.parse(payload);

  // 6. Single-use nonce: reject replays, mark first sight
  if (await options.nonceStore.seen(claims.nonce, claims.iss)) {
    throw new Error('nonce replay detected: ' + claims.nonce);
  }
  await options.nonceStore.mark(claims.nonce, claims.iss);

  return claims;
}

Labs that check themselves

Guided labs are declarative. An instructor defines checkpoints as Zod-validated predicates over simulation output: node_voltage, branch_current, waveform_match, circuit_contains, ac_gain_at. A pure TypeScript evaluator runs them against the student's vectors, no eval anywhere. Reference waveforms are generated at save time in the instructor's own browser by the same ngspice worker, then stored in R2 as CSV. When a student clears the last checkpoint, the score posts back to Canvas or Moodle through LTI grade passback, and the retry queue catches the submissions the LMS drops.

Undo in a shared circuit

Co-editing runs on Yjs CRDTs synced through a Cloudflare Durable Object. Component adds, edits, and moves converge between browsers, and a presence layer shows where lab partners are pointing. The detail that took real care was Ctrl+Z: a naive undo stack in a shared document reverts other people's work. Y.UndoManager is bridged into the editor's existing undo store so each user's history tracks only their own edits, verified with two-browser Playwright scenarios.

The lab section, before and after

Circuits lab today

Find the one lab machine with LTspice, or install it at home and hope IT never locked the download. Fight the schematic editor. Screenshot the waveform into a Word doc, export a PDF, upload to Canvas, wait a week for a TA to grade it by eye.

What OmniSpice is building toward

Click the assignment link in Canvas. The circuit opens in the browser, already authenticated, and simulates as you edit. Checkpoints verify each step against reference waveforms, and the grade posts back the moment you clear the last one.

Tech Stack

Full Stack Details

Frontend

React 19TypeScriptViteReact FlowuPlotZustand

Simulation

ngspice 45 (WASM)EmscriptenWeb Workers

Collaboration

YjsDurable Objectsy-indexeddb

Backend

HonoCloudflare WorkersD1R2Clerk

LMS Integration

LTI 1.3joseWeb Crypto

Where it stands

Pre-launch, and framed that way on purpose. The build is six roadmap phases and 42 plans, developed test-first: 70 unit and integration test files plus 29 Playwright specs, including two-browser CRDT convergence runs. The editor, the simulation lanes, collaboration, the LTI plumbing, and the lab engine all work. The classroom UI is 4 of 7 plans in, nothing is deployed, and there are no users to cite yet. What exists is the hard part; most of what remains is screens.

  • Draw, simulate, and read waveforms entirely client-side. Installs as a PWA and keeps working offline
  • Live DC operating point, debounced AC sweeps, commit-on-release transients
  • Self-hosted LTI 1.3 with automatic grade passback, Canvas and Moodle first
  • Real-time co-editing with per-user undo
  • LTspice .asc import so existing course materials survive the switch

Next case study