208 lines
5.9 KiB
JavaScript
208 lines
5.9 KiB
JavaScript
import { solveLinear, zeros } from './linalg.js';
|
|
|
|
const GROUND = 'gnd';
|
|
const Vt = 0.02585; // thermal voltage at 300 K
|
|
const GMIN = 1e-12; // minimum shunt conductance for numerical stability
|
|
|
|
/**
|
|
* Modified Nodal Analysis solver.
|
|
*
|
|
* Usage:
|
|
* const c = new Circuit();
|
|
* c.addComponent(new VoltageSource('V1', 'a', 'gnd', 5));
|
|
* c.addComponent(new Resistor('R1', 'a', 'b', 1000));
|
|
* c.addComponent(new Resistor('R2', 'b', 'gnd', 2000));
|
|
* c.solveDC();
|
|
* c.nodeVoltage('b'); // → 3.333 V
|
|
*/
|
|
export class Circuit {
|
|
constructor() {
|
|
this.components = [];
|
|
this.nodes = new Map(); // nodeName → index (gnd is not in the matrix)
|
|
this.vsources = []; // components that add extra MNA rows
|
|
this.state = {
|
|
nodeVoltages: {}, // nodeName → V (gnd = 0)
|
|
branchCurrents: {}, // vsourceName → I
|
|
prev: null, // previous-step state for transient
|
|
};
|
|
this.time = 0;
|
|
}
|
|
|
|
addComponent(c) {
|
|
this.components.push(c);
|
|
for (const n of c.nodes()) this._ensureNode(n);
|
|
return this;
|
|
}
|
|
|
|
removeComponent(name) {
|
|
this.components = this.components.filter(c => c.name !== name);
|
|
}
|
|
|
|
getComponent(name) {
|
|
return this.components.find(c => c.name === name);
|
|
}
|
|
|
|
_ensureNode(name) {
|
|
if (name === GROUND) return;
|
|
if (!this.nodes.has(name)) this.nodes.set(name, this.nodes.size);
|
|
}
|
|
|
|
_nodeIndex(name) {
|
|
if (name === GROUND) return -1;
|
|
return this.nodes.get(name);
|
|
}
|
|
|
|
/** Build and solve the DC system. Returns { nodeVoltages, branchCurrents }. */
|
|
solveDC(opts = {}) {
|
|
const maxIter = opts.maxIter ?? 100;
|
|
const tol = opts.tol ?? 1e-7;
|
|
|
|
this.vsources = this.components.filter(c => c.isVoltageSource);
|
|
const N = this.nodes.size;
|
|
const M = this.vsources.length;
|
|
const dim = N + M;
|
|
|
|
// Newton-Raphson for non-linear elements (diodes, LEDs, BJTs)
|
|
let x = new Array(dim).fill(0);
|
|
let converged = false;
|
|
|
|
// Reset non-linear device per-solve state so pnjlim starts clean
|
|
for (const c of this.components) {
|
|
if (c.isNonlinear && typeof c._resetIter === 'function') c._resetIter();
|
|
}
|
|
|
|
for (let iter = 0; iter < maxIter; iter++) {
|
|
const G = zeros(dim, dim);
|
|
const b = new Array(dim).fill(0);
|
|
|
|
// Tiny shunt to ground on every node for numerical stability
|
|
for (let i = 0; i < N; i++) G[i][i] += GMIN;
|
|
|
|
const ctx = {
|
|
nodeIndex: (n) => this._nodeIndex(n),
|
|
vsourceIndex: (name) => {
|
|
const idx = this.vsources.findIndex(v => v.name === name);
|
|
return idx < 0 ? -1 : N + idx;
|
|
},
|
|
nodeVoltageFromX: (n) => {
|
|
if (n === GROUND) return 0;
|
|
const i = this._nodeIndex(n);
|
|
return x[i] ?? 0;
|
|
},
|
|
dt: opts.dt,
|
|
prev: this.state.prev,
|
|
time: this.time,
|
|
iteration: iter,
|
|
};
|
|
|
|
for (const c of this.components) c.stampDC(G, b, ctx);
|
|
|
|
let xNew;
|
|
try {
|
|
xNew = solveLinear(G, b);
|
|
} catch (e) {
|
|
throw new Error(`DC solve failed at iteration ${iter}: ${e.message}`);
|
|
}
|
|
|
|
// Convergence check
|
|
let maxDelta = 0;
|
|
for (let i = 0; i < dim; i++) {
|
|
maxDelta = Math.max(maxDelta, Math.abs(xNew[i] - x[i]));
|
|
}
|
|
|
|
// Damping: if any diode/BJT node moves more than 0.2 V, limit the step
|
|
const dampedX = xNew.map((v, i) => {
|
|
const delta = v - x[i];
|
|
if (Math.abs(delta) > 0.5 && iter > 0) {
|
|
return x[i] + Math.sign(delta) * 0.5;
|
|
}
|
|
return v;
|
|
});
|
|
|
|
x = dampedX;
|
|
|
|
const hasNonlinear = this.components.some(c => c.isNonlinear);
|
|
if (!hasNonlinear) { converged = true; break; }
|
|
if (maxDelta < tol) { converged = true; break; }
|
|
}
|
|
|
|
if (!converged) {
|
|
// Attach warning but keep state
|
|
this.state.converged = false;
|
|
} else {
|
|
this.state.converged = true;
|
|
}
|
|
|
|
// Save results
|
|
this.state.nodeVoltages = { [GROUND]: 0 };
|
|
for (const [name, idx] of this.nodes) {
|
|
this.state.nodeVoltages[name] = x[idx];
|
|
}
|
|
this.state.branchCurrents = {};
|
|
for (let i = 0; i < this.vsources.length; i++) {
|
|
this.state.branchCurrents[this.vsources[i].name] = x[N + i];
|
|
}
|
|
return this.state;
|
|
}
|
|
|
|
/** Advance time by dt, solving transient using backward Euler. */
|
|
stepTransient(dt) {
|
|
this.state.prev = {
|
|
nodeVoltages: { ...this.state.nodeVoltages },
|
|
branchCurrents: { ...this.state.branchCurrents },
|
|
};
|
|
const res = this.solveDC({ dt });
|
|
this.time += dt;
|
|
return res;
|
|
}
|
|
|
|
/** Run transient from t=0 to tEnd with fixed dt. Returns array of snapshots. */
|
|
runTransient(tEnd, dt, sampleEvery = 1) {
|
|
const samples = [];
|
|
this.time = 0;
|
|
// Seed prev state from capacitor initial voltages.
|
|
const initVoltages = { gnd: 0 };
|
|
for (const [n] of this.nodes) initVoltages[n] = 0;
|
|
for (const comp of this.components) {
|
|
if (comp.Vinit !== undefined && typeof comp.a === 'string') {
|
|
initVoltages[comp.a] = (initVoltages[comp.b] ?? 0) + comp.Vinit;
|
|
}
|
|
}
|
|
this.state = {
|
|
nodeVoltages: { ...initVoltages },
|
|
branchCurrents: {},
|
|
prev: { nodeVoltages: { ...initVoltages }, branchCurrents: {} },
|
|
};
|
|
samples.push({ t: 0, nodeVoltages: { ...initVoltages }, branchCurrents: {} });
|
|
let n = 0;
|
|
while (this.time < tEnd - dt / 2) {
|
|
this.stepTransient(dt);
|
|
n++;
|
|
if (n % sampleEvery === 0) samples.push({ t: this.time, ...this._snapshot() });
|
|
}
|
|
return samples;
|
|
}
|
|
|
|
_snapshot() {
|
|
return {
|
|
nodeVoltages: { ...this.state.nodeVoltages },
|
|
branchCurrents: { ...this.state.branchCurrents },
|
|
};
|
|
}
|
|
|
|
nodeVoltage(name) {
|
|
return this.state.nodeVoltages[name] ?? 0;
|
|
}
|
|
|
|
branchCurrent(name) {
|
|
return this.state.branchCurrents[name] ?? 0;
|
|
}
|
|
|
|
reset() {
|
|
this.state = { nodeVoltages: {}, branchCurrents: {}, prev: null };
|
|
this.time = 0;
|
|
}
|
|
}
|
|
|
|
export { GROUND, Vt, GMIN };
|