velxio/frontend/src/simulation/spice/valueParser.ts

77 lines
2.5 KiB
TypeScript
Raw Normal View History

feat: electrical simulation via ngspice-WASM (eecircuit-engine) Adds full SPICE-accurate electrical simulation to Velxio, behind a lazy- loaded ⚡ toolbar toggle. Arduino / ESP32 / RP2040 sketches now co-simulate with real analog behaviour: correct voltages on wires, real I–V curves on LEDs, working potentiometers, NTC thermistors read by analogRead(), PWM driving RC filters, transistors, op-amps, diodes, MOSFETs, etc. Engine: eecircuit-engine (ngspice compiled to WebAssembly). Main bundle stays at 2.4 MB; the 20 MB SPICE chunk only loads when the user activates electrical mode. Disabled at build time via VITE_ELECTRICAL_SIM=false. Frontend additions: - simulation/spice/: SpiceEngine wrapper + lazy entry, NetlistBuilder with UnionFind over wires, componentToSpice mapping (24 metadataIds incl. real part numbers: 2N2222, 2N3055, BC547, IRF540, 2N7000, 1N4148, 1N4007, 1N4733, LEDs, NTC, op-amp ideal), CircuitScheduler with debounced coalescing, AVRSpiceBridge for quasi-static co-simulation. - store/useElectricalStore: Zustand slice, feature-flag aware. - components/analog-ui/: ⚡ toolbar toggle + SVG voltage overlay. - components/components-instruments/: Voltmeter, Ammeter probes. - 62 tests (spice-*, netlist-builder, component-to-spice, instruments). Sandbox (test/test_circuit/): 47-test validation sandbox that proved the approach (hand-rolled MNA baseline + ngspice pipeline) before porting to the app. Kept as reference. Docs: docs/wiki/circuit-emulation-*.md (13 engineering pages covering architecture, solvers, components, AVR bridge, gotchas, performance, integration plan, API reference, appendix) + electrical-simulation- user-guide.md (end-user facing). Reference plan: test/test_circuit/plan/phase_8_velxio_implementation.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:11:54 +07:00
/**
* Parse a value string with SPICE-style SI suffixes.
*
* "4.7k" 4700
* "220" 220
* "1Meg" 1_000_000
* "10u" 1e-5
* "100n" 1e-7
* "0.1u" 1e-7
* "22p" 2.2e-11
* "10mH" 0.01
* "1.5H" 1.5
*
* Recognized suffixes (case-insensitive for most; `Meg` is special):
* G = 1e9 T = 1e12 Meg = 1e6 K = 1e3
* m = 1e-3 u = 1e-6 n = 1e-9 p = 1e-12 f = 1e-15
*
* Trailing unit letters (Ω, F, H, V, A) are tolerated and stripped.
* Returns `fallback` if parsing fails.
*/
const SUFFIX_MAP: Array<[string, number]> = [
['meg', 1e6],
['g', 1e9],
['t', 1e12],
['k', 1e3],
['m', 1e-3],
['u', 1e-6],
['µ', 1e-6],
['n', 1e-9],
['p', 1e-12],
['f', 1e-15],
];
export function parseValueWithUnits(input: unknown, fallback = NaN): number {
if (typeof input === 'number' && Number.isFinite(input)) return input;
if (typeof input !== 'string') return fallback;
let s = input.trim();
if (!s) return fallback;
// Strip trailing physical unit words (non-SI-prefix ones)
s = s.replace(/(ohms?|Ω|farads?|henrys?|henries|amps?|volts?|ω|watts?)\s*$/i, '');
// Strip trailing single-letter unit (Ω, H, V, A, W). NOT F (which is femto in SPICE).
// Only strip when preceded by a digit or SI-prefix letter, not when the whole
// input is just the unit (so we don't turn "k" into "").
s = s.replace(/([0-9a-zµ])[ΩHVAW]$/i, '$1');
// Try suffix match (longest first — "meg" before "m")
const lower = s.toLowerCase();
for (const [suffix, mult] of SUFFIX_MAP) {
if (lower.endsWith(suffix)) {
const numPart = s.slice(0, s.length - suffix.length);
const n = parseFloat(numPart);
if (Number.isFinite(n)) return n * mult;
}
}
// Plain number
const n = parseFloat(s);
return Number.isFinite(n) ? n : fallback;
}
/** Format a number back into a compact SPICE-style string. */
export function formatValueWithUnits(value: number): string {
if (!Number.isFinite(value)) return '0';
const abs = Math.abs(value);
if (abs >= 1e9) return `${(value / 1e9).toPrecision(3)}G`;
if (abs >= 1e6) return `${(value / 1e6).toPrecision(3)}Meg`;
if (abs >= 1e3) return `${(value / 1e3).toPrecision(3)}k`;
if (abs >= 1) return `${value.toPrecision(3)}`;
if (abs >= 1e-3) return `${(value * 1e3).toPrecision(3)}m`;
if (abs >= 1e-6) return `${(value * 1e6).toPrecision(3)}u`;
if (abs >= 1e-9) return `${(value * 1e9).toPrecision(3)}n`;
if (abs >= 1e-12) return `${(value * 1e12).toPrecision(3)}p`;
return `${(value * 1e15).toPrecision(3)}f`;
}