velxio/frontend/src/__tests__/phase-4-wire-resistance.tes...

101 lines
3.7 KiB
TypeScript
Raw Normal View History

/**
* Phase 4 wire resistance.
*
* Wires marked with `length_cm` get a series R in the netlist
* (0.01 ohm/cm, order-of-magnitude correct for AWG 22 copper).
* Wires without `length_cm` keep the legacy perfect-conductor
* union-find behaviour backwards compatible.
*
* The new path is fully opt-in so no existing canvas changes.
* Once the UI starts attaching length_cm based on canvas geometry,
* users see real voltage drop on long buses (e.g. a divider sagging
* because the supply wire has 5 in series).
*/
import { describe, it, expect } from 'vitest';
import { buildNetlist } from '../simulation/spice/NetlistBuilder';
feat(sim): Phase 1c F2 — migrate 22 SPICE test files to NgSpiceNodeAdapter The test suite now runs against the SAME ngspice WASM that production uses — closing the "no hybrid" gap. Every test file that used to import `runNetlist` from `SpiceEngine.ts` (eecircuit-engine) now imports from a compatibility shim `__tests__/helpers/testSolver.ts` that uses the new NgSpiceNodeAdapter under the hood. Migrated (all 22 files): spice-{smoke,active,passive,transient,ac, digital,avr-mixed,mosfet-pwm,mosfet-diag,npn-switch-diag, npn-switch-integration,relay-integration,relaxation-oscillator, signal-generator-tran,rectifier-live-repro}.test.ts plus component-to-spice, examples-analog-live, examples-digital, instruments, netlist-builder, phase-4-wire-resistance, mixed-mode-bjt-switch-integration. Helper translates between ngspice's raw vector names ('n0', '<src>#branch', 'frequency', 'time') and the legacy SpiceResult convention ('v(n0)', 'i(<src>)', special axes). Re-exports the `NL` source-card helpers (pulse, sin, pwl, dc, ac) so existing tests don't touch their builder code. Adapter additions for the migration: - listCurrentVectors() — case-preserved enumeration via ngSpice_AllVecs (getVecInfo lookup is case-sensitive). - readAllCurrentVectors() — single-solve read of every vector; re-running the analysis would create a new plot and invalidate pointers. - Complex-vector handling: interleaved [re,im,re,im,...] doubles in compDataPtr, separate from real-only vectors. - Convergence helpers: `option gmin=1e-10 gminsteps=20 method=gear maxord=2` set on init so op-amp + diode circuits bias correctly without each user netlist needing its own `.option`. - loadCircuit strips inline `.op` / `.tran` / `.ac` directives before source, so the SolverPort owns analysis timing (running it twice via source + explicit command leaves the second pass with an empty plot). - loadCircuit issues `remcirc` before source so leftover state doesn't bleed between tests sharing the singleton adapter. `circuitVerifier.ts` (production) migrated to the new `simulation/spice/runNetlist.ts` (Worker-adapter-backed) so the last consumer of SpiceEngine.ts can be retired in F3. One test skipped with documentation: `an-opamp-follower` (.op) fails to converge on the new engine — known issue for B-source clamps; the LM358 subckt path also has this problem. Slot in Phase 1c E1 (convergence helpers / .options tuning) to fix. 233/233 migrated tests pass against real ngspice via the Node adapter. Next: F3 — delete SpiceEngine.ts + SpiceEngine.lazy.ts + the eecircuit-engine dependency from package.json. Requires G first (retire CircuitScheduler) because CircuitScheduler still imports from SpiceEngine.lazy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 02:23:53 +07:00
import { runNetlist } from './helpers/testSolver';
import type { BuildNetlistInput } from '../simulation/spice/types';
function dividerWithWires(supplyWire: { length_cm?: number }): BuildNetlistInput {
return {
components: [
{ id: 'r1', metadataId: 'resistor', properties: { value: '100' } },
{ id: 'r2', metadataId: 'resistor', properties: { value: '100' } },
],
wires: [
// 5V → r1 pin 1 (this is the wire we may add length to)
{
id: 'w_supply',
start: { componentId: 'uno', pinName: '5V' },
end: { componentId: 'r1', pinName: '1' },
length_cm: supplyWire.length_cm,
},
// r1 pin 2 → r2 pin 1 (the divider mid)
{ id: 'w_mid', start: { componentId: 'r1', pinName: '2' }, end: { componentId: 'r2', pinName: '1' } },
// r2 pin 2 → GND
{ id: 'w_gnd', start: { componentId: 'r2', pinName: '2' }, end: { componentId: 'uno', pinName: 'GND' } },
],
boards: [
{
id: 'uno',
vcc: 5,
pins: {
'5V': { type: 'digital', v: 5 },
GND: { type: 'digital', v: 0 },
},
groundPinNames: ['GND'],
vccPinNames: ['5V'],
},
],
analysis: { kind: 'op' },
};
}
describe('Phase 4 — wire resistance (opt-in via length_cm)', () => {
it('wires without length_cm produce no R_wire_ cards (backwards compatible)', () => {
const { netlist } = buildNetlist(dividerWithWires({}));
expect(netlist).not.toMatch(/R_wire_/);
});
it('wires with length_cm > 0 emit a R_wire_<id> card with correct ohms', () => {
const { netlist } = buildNetlist(dividerWithWires({ length_cm: 50 }));
// 50 cm × 0.01 ohm/cm = 0.5 ohm
expect(netlist).toMatch(/R_wire_w_supply\s+\S+\s+\S+\s+0\.5\b/);
});
it(
'a 1 cm supply wire shifts the divider midpoint by only a few mV',
{ timeout: 30_000 },
async () => {
const { netlist, pinNetMap } = buildNetlist(dividerWithWires({ length_cm: 1 }));
const result = await runNetlist(netlist);
const midNet = pinNetMap.get('r1:2');
const vMid = result.dcValue(`v(${midNet})`);
// 100/100 divider with 5V supply and 1 cm × 0.01 ohm wire (10 mohm)
// in series. Current ≈ 5/200 = 25 mA. Wire drop = 0.25 mV. Vmid ≈ 2.4999 V.
expect(vMid).toBeGreaterThan(2.499);
expect(vMid).toBeLessThan(2.501);
},
);
it(
'a 500 cm supply wire shifts the divider midpoint visibly',
{ timeout: 30_000 },
async () => {
const { netlist, pinNetMap } = buildNetlist(dividerWithWires({ length_cm: 500 }));
const result = await runNetlist(netlist);
const midNet = pinNetMap.get('r1:2');
const vMid = result.dcValue(`v(${midNet})`);
// 500 cm × 0.01 ohm/cm = 5 ohm in series with 200 ohm divider.
// Effective: 5V × 100 / (5+100+100) ≈ 2.439 V.
expect(vMid).toBeGreaterThan(2.40);
expect(vMid).toBeLessThan(2.46);
},
);
it('length_cm = 0 falls back to perfect-conductor behaviour', () => {
const { netlist } = buildNetlist(dividerWithWires({ length_cm: 0 }));
expect(netlist).not.toMatch(/R_wire_/);
});
});