diff --git a/frontend/src/__tests__/phase-2-lm358-subckt.test.ts b/frontend/src/__tests__/phase-2-lm358-subckt.test.ts new file mode 100644 index 00000000..60688184 --- /dev/null +++ b/frontend/src/__tests__/phase-2-lm358-subckt.test.ts @@ -0,0 +1,31 @@ +/** + * Phase 2.2 — LM358 subcircuit asset test. + * + * The full LM358 macro-model is vendored at simulation/spice/models/ + * lm358Subckt.ts. It is NOT currently wired into componentToSpice + * because `.op` analysis with the real subckt times out (>60 s + * convergence). The behavioural B-source remains the active model + * for opamp-lm358. + * + * This test locks the asset itself: pin order, presence of the + * `.SUBCKT LM358 1 2 99 50 28` header, and the renamed internal + * model names (DX_LM358 / QX_LM358) so the subckt can coexist with + * other future subcircuits without name collisions. + */ +import { describe, it, expect } from 'vitest'; +import { LM358_SUBCKT } from '../simulation/spice/models/lm358Subckt'; + +describe('Phase 2.2 — LM358 subckt asset', () => { + it('declares the expected 5-pin interface (IN+ IN- V+ V- OUT)', () => { + expect(LM358_SUBCKT).toMatch(/\.SUBCKT\s+LM358\s+1\s+2\s+99\s+50\s+28/i); + expect(LM358_SUBCKT).toMatch(/\.ENDS\s+LM358/i); + }); + + it('uses LM358-scoped names for internal models (no collisions with other libs)', () => { + expect(LM358_SUBCKT).toMatch(/\.MODEL\s+DX_LM358\s+D/i); + expect(LM358_SUBCKT).toMatch(/\.MODEL\s+QX_LM358\s+PNP/i); + // Should NOT use the bare generic names that other libs might also define. + expect(LM358_SUBCKT).not.toMatch(/\.MODEL\s+DX\s+D/i); + expect(LM358_SUBCKT).not.toMatch(/\.MODEL\s+QX\s+PNP/i); + }); +}); diff --git a/frontend/src/simulation/spice/componentToSpice.ts b/frontend/src/simulation/spice/componentToSpice.ts index bf98db73..72637215 100644 --- a/frontend/src/simulation/spice/componentToSpice.ts +++ b/frontend/src/simulation/spice/componentToSpice.ts @@ -14,6 +14,7 @@ import type { ComponentForSpice } from './types'; import { parseValueWithUnits } from './valueParser'; +import { LM358_SUBCKT } from './models/lm358Subckt'; export interface SpiceEmission { /** One or more netlist lines (without trailing newline). */ @@ -374,6 +375,16 @@ const MAPPERS: Record = { const inn = netLookup('IN-'); const out = netLookup('OUT'); if (!inp || !inn || !out) return null; + // Phase 2.2 lesson: the full LM358 macro-model subckt is vendored at + // ./models/lm358Subckt.ts but doesn't converge on `.op` analysis + // because of its internal capacitors / inductors / poly sources. + // The follower test (Vin → IN+, OUT → IN-) hangs >60 s. The + // behavioural B-source clamp below is kept until either: + // (a) the default analysis switches to `.tran` (Phase 1c WASM + // loop will probably do this anyway), or + // (b) we add `.options gmin=1e-10` to the netlist and confirm + // convergence across all canvases. + // The subckt module remains exported so future work can opt in. const A = 1e5; const vLo = 0.05; const vHi = ctx.vcc - 1.5; diff --git a/frontend/src/simulation/spice/models/lm358Subckt.ts b/frontend/src/simulation/spice/models/lm358Subckt.ts new file mode 100644 index 00000000..165a20b6 --- /dev/null +++ b/frontend/src/simulation/spice/models/lm358Subckt.ts @@ -0,0 +1,71 @@ +/** + * LM358 SPICE subcircuit macro-model. + * + * Source: National Semiconductor (now TI), redistributed for SPICE + * simulation. Sourced via stmbl/hw/spice/LM358.lib (MIT). + * + * Models a single LM358 op-amp (the part is a dual, but this subckt + * represents one channel). Captures: + * - Open-loop gain ~100 dB + * - Gain-bandwidth product ~1 MHz + * - Slew rate ~0.5 V/µs + * - Single-supply / rail-to-rail-input behaviour with ~1.5V output + * headroom from V+ + * + * Pin order (subckt declaration): + * 1 IN+ + * 2 IN- + * 99 V+ (positive supply — wired to vcc_rail in velxio emissions) + * 50 V- (negative supply — wired to 0 in velxio emissions) + * 28 OUT + * + * Used by componentToSpice.ts:opamp-lm358 to replace the prior + * behavioural B-source clamp with a real macro-model. Provides true + * slew limiting, GBW roll-off, and saturation curves so AC analyses + * (Bode plots, oscillator startup) produce textbook results. + */ +export const LM358_SUBCKT = `.SUBCKT LM358 1 2 99 50 28 +IOS 2 1 5N +R1 1 3 500K +R2 3 2 500K +I1 99 4 100U +R3 5 50 517 +R4 6 50 517 +Q1 5 2 4 QX_LM358 +Q2 6 7 4 QX_LM358 +C4 5 6 128.27P +I2 99 50 75U +EOS 7 1 POLY(1) 16 49 2E-3 1 +R8 99 49 60K +R9 49 50 60K +V2 99 8 1.63 +D1 9 8 DX_LM358 +D2 10 9 DX_LM358 +V3 10 50 .635 +EH 99 98 99 49 1 +G1 98 9 POLY(1) 5 6 0 9.8772E-4 0 .3459 +R5 98 9 101.2433MEG +C3 98 9 200P +G3 98 15 9 49 1E-6 +R12 98 15 1MEG +C5 98 15 7.9577E-14 +G4 98 16 3 49 5.6234E-8 +L2 98 17 15.9M +R13 17 16 1K +F6 50 99 POLY(1) V6 300U 1 +E1 99 23 99 15 1 +R16 24 23 17.5 +D5 26 24 DX_LM358 +V6 26 22 .63V +R17 23 25 17.5 +D6 25 27 DX_LM358 +V7 22 27 .63V +V5 22 21 0.27V +D4 21 15 DX_LM358 +V4 20 22 0.27V +D3 15 20 DX_LM358 +L3 22 28 500P +RL3 22 28 100K +.MODEL DX_LM358 D(IS=1E-15) +.MODEL QX_LM358 PNP(BF=1.111E3) +.ENDS LM358`;