test(sim): Phase 2 lockdown — guard BJT/diode model upgrades

Asserts the Phase 2 BJTs include Gummel-Poon junction caps (CJC/CJE)
and forward transit time (TF), and the diode upgrades include
reverse-recovery time (tt) and Schottky band-gap (Eg). If anyone
simplifies the models in the future, these regress fail and surface
the loss of AC/transient fidelity.

Also guards the dedupe identity between the canonical diode-1n4148
emission and the relay flyback diode — they must serialise as the same
string or ngspice will reject the netlist for duplicate .model lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
davidmonterocrespo24 2026-05-15 16:59:39 +02:00
parent c476084e00
commit 7508423c6e
1 changed files with 87 additions and 0 deletions

View File

@ -0,0 +1,87 @@
/**
* Phase 2 lockdown verify the BJT and diode model upgrades survived
* any later edits to componentToSpice.ts. Asserts that the upgraded
* models include the parameters that distinguish them from the old
* truncated SPICE3F5 placeholders.
*
* If these fail, someone reasonably simplified the models again that
* regresses AC/transient fidelity. Either re-upgrade the model or
* remove this test consciously.
*/
import { describe, it, expect } from 'vitest';
import { componentToSpice } from '../simulation/spice/componentToSpice';
import type { PlacedComponent } from '../simulation/spice/types';
function netLookupStub(_pin: string): string {
return 'net_dummy';
}
function emit(metadataId: string, properties: Record<string, unknown> = {}): string[] {
const comp: PlacedComponent = { id: 't1', metadataId, properties };
const result = componentToSpice(comp, netLookupStub, { vcc: 5 });
if (!result) throw new Error(`no emission for ${metadataId}`);
return Array.from(result.modelsUsed);
}
describe('Phase 2 — BJT Gummel-Poon parameters present', () => {
it.each([
['bjt-2n2222', 'Q2N2222'],
['bjt-bc547', 'QBC547'],
['bjt-2n3055', 'Q2N3055'],
['bjt-2n3906', 'Q2N3906'],
['bjt-bc557', 'QBC557'],
])('%s model includes Gummel-Poon junction caps and transit times', (id, modelName) => {
const models = emit(id);
const m = models.find((s) => s.includes(modelName));
expect(m, `no model card found for ${modelName}`).toBeDefined();
const text = (m ?? '').toUpperCase();
// Junction caps — separates Phase 2 model from the old truncated one.
expect(text).toMatch(/CJC=/);
expect(text).toMatch(/CJE=/);
// Forward transit time — required for switching/AC behaviour.
expect(text).toMatch(/TF=/);
// Bf must still be there too (sanity).
expect(text).toMatch(/BF=/);
});
});
describe('Phase 2 — diode reverse-recovery and junction capacitance', () => {
it('D1N4148 includes tt (storage time) and Cjo', () => {
const models = emit('diode-1n4148');
const m = models.find((s) => s.includes('D1N4148'));
expect(m).toBeDefined();
expect(m).toMatch(/tt=/i);
expect(m).toMatch(/Cjo=/i);
});
it('Schottky 1N5817 carries Eg (Schottky band-gap) and Cjo', () => {
const models = emit('diode-1n5817');
const m = models.find((s) => s.includes('D1N5817'));
expect(m).toBeDefined();
expect(m).toMatch(/Eg=/i);
expect(m).toMatch(/Cjo=/i);
});
it('Schottky 1N5819 carries Eg and Cjo', () => {
const models = emit('diode-1n5819');
const m = models.find((s) => s.includes('D1N5819'));
expect(m).toBeDefined();
expect(m).toMatch(/Eg=/i);
expect(m).toMatch(/Cjo=/i);
});
});
describe('Phase 2 — relay flyback keeps the canonical D1N4148 string', () => {
it('relay emits the same D1N4148 .model string as a standalone diode', () => {
const standaloneModels = emit('diode-1n4148');
const relayModels = emit('relay');
const standalone = standaloneModels.find((s) => s.includes('D1N4148'));
const fromRelay = relayModels.find((s) => s.includes('D1N4148'));
expect(standalone).toBeDefined();
expect(fromRelay).toBeDefined();
// Must match exactly — the netlist dedupe Set collapses them by string
// identity. If they ever diverge, ngspice will see duplicate .model
// definitions and error out.
expect(fromRelay).toBe(standalone);
});
});