velxio/test/test_circuit/test/spice_mapped_relay.test.js

121 lines
3.7 KiB
JavaScript

import { describe, it, expect } from 'vitest';
import { runNetlist } from '../src/spice/SpiceEngine.js';
/**
* Sanity check for the relay mapper (fase 10.1).
*
* The mapper emits:
* - R + L for the coil
* - S (SW) element for the NO contact (closes when V_coil > Vt)
* - B + S pair for the NC contact (B inverts V_coil as control to a
* standard SW — no "normally closed" SW in ngspice)
* - Optional flyback diode
*
* Tests exercise:
* - Coil de-energised: NC closed, NO open
* - Coil energised: NC open, NO closed
* - Hysteresis: V_coil in the dead band doesn't chatter
*/
const MODELS_5V = `
.model RELAY_SW SW(Vt=3 Vh=0.75 Ron=0.05 Roff=1G)
.model D1N4148 D(Is=2.52n N=1.752 Rs=0.568)
`;
describe('componentToSpice — relay: de-energised', () => {
it('contacts: NC closed → COM == NC; NO open → COM ≠ NO', { timeout: 30_000 }, async () => {
// Drive signal: COM fed with 12V. NC pin has 1k load to ground, NO pin has 1k load.
// De-energised: NC closes → current flows com → nc → R → GND.
const netlist = `relay idle
Vcoil coil_p 0 DC 0
Vcom com 0 DC 12
R_nc_load nc 0 1k
R_no_load no 0 1k
R_coil coil_p 0 70
L_coil coil_p 0 20m
S_no com no coil_p 0 RELAY_SW
B_ncctrl ncctrl 0 V = 5 - (V(coil_p) - V(0))
S_nc com nc ncctrl 0 RELAY_SW
${MODELS_5V}
.op
.end`;
const { dcValue } = await runNetlist(netlist);
// NC closed → nc sees COM (≈ 12V through near-zero switch R + 1k load,
// so NC is very close to COM).
expect(dcValue('v(nc)')).toBeGreaterThan(11.9);
// NO open → no sees the pull-down (≈ 0V through 1G switch)
expect(dcValue('v(no)')).toBeLessThan(0.1);
});
});
describe('componentToSpice — relay: energised', () => {
it('contacts: NO closed → COM == NO; NC open → COM ≠ NC', { timeout: 30_000 }, async () => {
const netlist = `relay on
Vcoil coil_p 0 DC 5
Vcom com 0 DC 12
R_nc_load nc 0 1k
R_no_load no 0 1k
R_coil coil_p 0 70
L_coil coil_p 0 20m
S_no com no coil_p 0 RELAY_SW
B_ncctrl ncctrl 0 V = 5 - (V(coil_p) - V(0))
S_nc com nc ncctrl 0 RELAY_SW
${MODELS_5V}
.op
.end`;
const { dcValue } = await runNetlist(netlist);
expect(dcValue('v(no)')).toBeGreaterThan(11.9);
expect(dcValue('v(nc)')).toBeLessThan(0.1);
});
});
describe('componentToSpice — relay: threshold behaviour', () => {
it('V_coil below Vt-Vh (= 2.25V): relay stays de-energised', { timeout: 30_000 }, async () => {
const netlist = `relay marginal-low
Vcoil coil_p 0 DC 2
Vcom com 0 DC 12
R_no_load no 0 1k
R_coil coil_p 0 70
L_coil coil_p 0 20m
S_no com no coil_p 0 RELAY_SW
${MODELS_5V}
.op
.end`;
const { dcValue } = await runNetlist(netlist);
// 2V < Vt=3 - Vh=0.75 → switch stays OFF → NO open → V(no) ≈ 0
expect(dcValue('v(no)')).toBeLessThan(0.1);
});
it('V_coil above Vt+Vh (= 3.75V): relay energises', { timeout: 30_000 }, async () => {
const netlist = `relay marginal-high
Vcoil coil_p 0 DC 4
Vcom com 0 DC 12
R_no_load no 0 1k
R_coil coil_p 0 70
L_coil coil_p 0 20m
S_no com no coil_p 0 RELAY_SW
${MODELS_5V}
.op
.end`;
const { dcValue } = await runNetlist(netlist);
// 4V > Vt+Vh=3.75 → switch CLOSED
expect(dcValue('v(no)')).toBeGreaterThan(11.9);
});
});
describe('componentToSpice — relay: coil current', () => {
it('nominal 5V coil draws ≈ 71 mA through 70Ω', { timeout: 30_000 }, async () => {
const netlist = `relay coil I
Vcoil coil_p 0 DC 5
R_coil coil_p 0 70
L_coil coil_p 0 20m
.op
.end`;
const { vec } = await runNetlist(netlist);
const i = vec('i(vcoil)')[0];
// I = 5V/70Ω = 71.4 mA (sign depends on reference direction)
expect(Math.abs(i)).toBeGreaterThan(0.06);
expect(Math.abs(i)).toBeLessThan(0.08);
});
});