From 47ff9b732ebe48183dd9614963d66cde11ad8eb0 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sun, 1 Jan 2012 19:26:48 -0500 Subject: [PATCH] cjt's simulator --- js/cktsim.js | 885 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 885 insertions(+) create mode 100644 js/cktsim.js diff --git a/js/cktsim.js b/js/cktsim.js new file mode 100644 index 0000000000..e61beee0f0 --- /dev/null +++ b/js/cktsim.js @@ -0,0 +1,885 @@ +////////////////////////////////////////////////////////////////////////////// +// +// Circuit simulator +// +////////////////////////////////////////////////////////////////////////////// + +// Chris Terman, Dec. 2011 + +// create a circuit for simulation using "new cktsim.Circuit()" + +// for modified nodal analysis (MNA) stamps see +// http://books.google.com/books?id=qhHsSlazGrQC&pg=PA44&lpg=PA44&dq=MNA+stamp+inductor&source=bl&ots=ThMq-FmhLo&sig=cTP1ld_fhIJbGPSBXPDbh3Xappk&hl=en&sa=X&ei=6wb-ToecFMHj0QH61-Fs&ved=0CFcQ6AEwAw#v=onepage&q=MNA%20stamp%20inductor&f=false + +cktsim = (function() { + + /////////////////////////////////////////////////////////////////////////////// + // + // Circuit + // + ////////////////////////////////////////////////////////////////////////////// + + // types of "nodes" in the linear system + T_VOLTAGE = 0; + T_CURRENT = 1; + + function Circuit() { + this.node_map = new Array(); + this.ntypes = []; + this.initial_conditions = []; // ic's for each element + + this.devices = []; // list of devices + this.device_map = new Array(); // map name -> device + this.end_of_timestep = []; // list of devices to be called at end of each timestep + + this.finalized = false; + this.node_index = -1; + + // for backward Euler: coeff0 = 1/timestep, coeff1 = 0 + // for trapezoidal: coeff0 = 2/timestep, coeff1 = 1 + this.coeff0 = undefined; + this.coeff1 = undefined; + } + + // index of ground node + Circuit.prototype.gnd_node = function() { + return -1; + } + + // allocate a new node index + Circuit.prototype.node = function(name,ntype,ic) { + this.node_index += 1; + if (name) this.node_map[name] = this.node_index; + this.ntypes.push(ntype); + this.initial_conditions.push(ic); + return this.node_index; + } + + // call to finalize the circuit in preparation for simulation + Circuit.prototype.finalize = function() { + if (!this.finalized) { + this.finalized = true; + this.N = this.node_index + 1; // number of nodes + + // give each device a chance to finalize itself + for (var i = this.devices.length - 1; i >= 0; --i) + this.devices[i].finalize(this); + + // set up augmented matrix and various temp vectors + this.matrix = new Array(this.N); + for (var i = this.N - 1; i >= 0; --i) + this.matrix[i] = new Array(this.N + 1); + this.swap = new Array(this.N); // keep track of row swaps during pivoting + this.soln = new Array(this.N); // hold swapped solution + } + } + + // load circuit from JSON netlist (see schematic.js) + Circuit.prototype.load_netlist = function(netlist) { + // set up mapping for ground node always called '0' in JSON netlist + this.node_map['0'] = this.gnd_node(); + + // process each component in the JSON netlist (see schematic.js for format) + for (var i = netlist.length - 1; i >= 0; --i) { + var component = netlist[i]; + var type = component[0]; + + // ignore wires, ground connections and view info + if (type == 'view' || type == 'w' || type == 'g') continue; + + var properties = component[2]; + var name = properties['name']; + + // convert node names to circuit indicies + var connections = component[3]; + for (var j = connections.length - 1; j >= 0; --j) { + var node = connections[j]; + var index = this.node_map[node]; + if (index == undefined) index = this.node(node,T_VOLTAGE); + connections[j] = index; + } + + // process the component + if (type == 'r') // resistor + this.r(connections[0],connections[1],properties['r'],name); + else if (type == 'c') // capacitor + this.c(connections[0],connections[1],properties['c'],name); + else if (type == 'l') // inductor + this.l(connections[0],connections[1],properties['l'],name); + else if (type == 'v') // voltage source + this.v(connections[0],connections[1],properties['value'],name); + else if (type == 'i') // current source + this.i(connections[0],connections[1],properties['value'],name); + else if (type == 'o') // op amp + this.opamp(connections[0],connections[1],connections[2],properties['A'],name); + else if (type == 'n') // n fet + this.fet('n',connections[0],connections[1],connections[2], + properties['sw'],properties['sl'],name); + else if (type == 'p') // p fet + this.fet('p',connections[0],connections[1],connections[2], + properties['sw'],properties['sl'],name); + } + } + + // DC analysis + Circuit.prototype.dc = function() { + this.finalize(); + + // set up equations + this.initialize_linear_system(); + for (var i = this.devices.length - 1; i >= 0; --i) + this.devices[i].load_dc(this); + + // solve for operating point + var x = solve_linear_system(this.matrix); + + // create solution dictionary + var result = new Array(); + for (var name in this.node_map) { + var index = this.node_map[name]; + result[name] = (index == -1) ? 0 : x[index]; + } + return result; + } + + Circuit.prototype.r = function(n1,n2,v,name) { + // try to convert string value into numeric value, barf if we can't + if ((typeof v) == 'string') { + v = parse_number(v,undefined); + if (v === undefined) return undefined; + } + + var d; + if (v != 0) { + d = new Resistor(n1,n2,v); + this.devices.push(d); + if (name) this.device_map[name] = d; + } else return this.v(n1,n2,0,name); // zero resistance == 0V voltage source + } + + Circuit.prototype.c = function(n1,n2,v,name) { + // try to convert string value into numeric value, barf if we can't + if ((typeof v) == 'string') { + v = parse_number(v,undefined); + if (v === undefined) return undefined; + } + var d = new Capacitor(n1,n2,v); + this.devices.push(d); + if (name) this.device_map[name] = d; + return d; + } + + Circuit.prototype.l = function(n1,n2,v,name) { + // try to convert string value into numeric value, barf if we can't + if ((typeof v) == 'string') { + v = parse_number(v,undefined); + if (v === undefined) return undefined; + } + var branch = this.node(undefined,T_CURRENT); + var d = new Inductor(n1,n2,branch,v); + this.devices.push(d); + if (name) this.device_map[name] = d; + return d; + } + + Circuit.prototype.v = function(n1,n2,v,name) { + var branch = this.node(undefined,T_CURRENT); + var d = new VSource(n1,n2,branch,v); + this.devices.push(d); + if (name) this.device_map[name] = d; + return d; + } + + Circuit.prototype.i = function(n1,n2,v,name) { + var d = new ISource(n1,n2,v); + this.devices.push(d); + if (name) this.device_map[name] = d; + return d; + } + + /////////////////////////////////////////////////////////////////////////////// + // + // Support for creating and solving a system of linear equations + // + //////////////////////////////////////////////////////////////////////////////// + + // model circuit using a linear system of the form Ax = b where + // A is an nxn matrix of conductances and branch voltages + // b is an n-element vector of sources + // x is an n-element vector of unknowns (node voltages, branch currents) + + // Knowns (A and b) are stored in an augmented matrix M = [A | b] + // Matrix is stored as an array of arrays: M[row][col]. + + // set augmented matrix to zero + Circuit.prototype.initialize_linear_system = function() { + for (var i = this.N - 1; i >= 0; --i) { + var row = this.matrix[i]; + for (var j = this.N; j >= 0; --j) // N+1 entries + row[j] = 0; + } + } + + // add conductance between two nodes to matrix A. + // Index of -1 refers to ground node + Circuit.prototype.add_conductance = function(i,j,g) { + if (i >= 0) { + this.matrix[i][i] += g; + if (j >= 0) { + this.matrix[i][j] -= g; + this.matrix[j][i] -= g; + this.matrix[j][j] += g; + } + } else if (j >= 0) + this.matrix[j][j] += g; + } + + // add individual conductance to A + Circuit.prototype.add_to_A = function(i,j,v) { + if (i >=0 && j >= 0) + this.matrix[i][j] += v; + } + + // add source info to vector b + Circuit.prototype.add_to_b = function(i,v) { + if (i >= 0) this.matrix[i][this.N] += v; + } + + // solve Ax=b and return vector x given augmented matrix [A | b] + // Uses Gaussian elimination with partial pivoting + function solve_linear_system(M) { + var N = M.length; // augmented matrix M has N rows, N+1 columns + var temp,i,j; + + // gaussian elimination + for (var col = 0; col < N ; col++) { + // find pivot: largest abs(v) in this column of remaining rows + var max_v = Math.abs(M[col][col]); + var max_col = col; + for (i = col + 1; i < N; i++) { + temp = Math.abs(M[i][col]); + if (temp > max_v) { max_v = temp; max_col = i; } + } + + // if no value found, generate a small conductance to gnd + // otherwise swap current row with pivot row + if (max_v == 0) M[col][col] = 1e-10; + else { + temp = M[col]; + M[col] = M[max_col]; + M[max_col] = temp; + } + + // now eliminate this column for all subsequent rows + for (i = col + 1; i < N; i++) { + temp = M[i][col]/M[col][col]; // multiplier we'll use for current row + if (temp != 0) + // subtract current row from row we're working on + // remember to process b too! + for (j = col; j <= N; j++) M[i][j] -= M[col][j]*temp; + } + } + + // matrix is now upper triangular, so solve for elements of x starting + // with the last row + var x = new Array(N); + for (i = N-1; i >= 0; --i) { + temp = M[i][N]; // grab b[i] from augmented matrix as RHS + // subtract LHS term from RHS using known x values + for (j = N-1; j > i; --j) temp -= M[i][j]*x[j]; + // now compute new x value + x[i] = temp/M[i][i]; + } + + // return solution + return x; + } + + // test solution code, expect x = [2,3,-1] + //M = [[2,1,-1,8],[-3,-1,2,-11],[-2,1,2,-3]]; + //x = solve_linear_system(M); + //y = 1; // so we have place to set a breakpoint :) + + /////////////////////////////////////////////////////////////////////////////// + // + // Device base class + // + //////////////////////////////////////////////////////////////////////////////// + + function Device() { + } + + // complete initial set up of device + Device.prototype.finalize = function() { + } + + // reset internal state of the device to initial value + Device.prototype.reset = function() { + } + + // load linear system equations for dc analysis + // (inductors shorted and capacitors opened) + Device.prototype.load_dc = function(ckt) { + } + + // load linear system equations for tran analysis + Device.prototype.load_tran = function(ckt,soln) { + } + + // load linear system equations for ac analysis: + // current sources open, voltage sources shorted + // linear models at operating point for everyone else + Device.prototype.load_ac = function(ckt) { + } + + // called with there's an accepted time step + Device.prototype.end_of_timestep = function(ckt) { + } + + // return time of next breakpoint for the device + Device.prototype.breakpoint = function(time) { + return undefined; + } + + /////////////////////////////////////////////////////////////////////////////// + // + // Parse numbers in engineering notation + // + /////////////////////////////////////////////////////////////////////////////// + + // convert first character of argument into an integer + function ord(ch) { + return ch.charCodeAt(0); + } + + // convert string argument to a number, accepting usual notations + // (hex, octal, binary, decimal, floating point) plus engineering + // scale factors (eg, 1k = 1000.0 = 1e3). + // return default if argument couldn't be interpreted as a number + function parse_number(s,default_v) { + s = s.toLowerCase(); // make life simple for ourselves + var slen = s.length; + var multiplier = 1; + var result = 0; + var index = 0; + + // skip leading whitespace + while (index < slen && s.charAt(index) <= ' ') index += 1; + if (index == slen) return default_v; + + // check for leading sign + if (s.charAt(index) == '-') { + multiplier = -1; + index += 1; + } else if (s.charAt(index) == '+') + index += 1; + var start = index; // remember where digits start + + // if leading digit is 0, check for hex, octal or binary notation + if (index >= slen) return default_v; + else if (s.charAt(index) == '0') { + index += 1; + if (index >= slen) return 0; + if (s.charAt(index) == 'x') { // hex + while (true) { + index += 1; + if (index >= slen) break; + if (s.charAt(index) >= '0' && s.charAt(index) <= '9') + result = result*16 + ord(s.charAt(index)) - ord('0'); + else if (s.charAt(index) >= 'a' && s.charAt(index) <= 'f') + result = result*16 + ord(s.charAt(index)) - ord('a') + 10; + else break; + } + return result*multiplier; + } else if (s.charAt(index) == 'b') { // binary + while (true) { + index += 1; + if (index >= slen) break; + if (s.charAt(index) >= '0' && s.charAt(index) <= '1') + result = result*2 + ord(s.charAt(index)) - ord('0'); + else break; + } + return result*multiplier; + } else if (s.charAt(index) != '.') { // octal + while (true) { + if (s.charAt(index) >= '0' && s.charAt(index) <= '7') + result = result*8 + ord(s.charAt(index)) - ord('0'); + else break; + index += 1; + if (index >= slen) break; + } + return result*multiplier; + } + } + + // read decimal integer or floating-point number + while (true) { + if (s.charAt(index) >= '0' && s.charAt(index) <= '9') + result = result*10 + ord(s.charAt(index)) - ord('0'); + else break; + index += 1; + if (index >= slen) break; + } + + // fractional part? + if (index < slen && s.charAt(index) == '.') { + while (true) { + index += 1; + if (index >= slen) break; + if (s.charAt(index) >= '0' && s.charAt(index) <= '9') { + result = result*10 + ord(s.charAt(index)) - ord('0'); + multiplier *= 0.1; + } else break; + } + } + + // if we haven't seen any digits yet, don't check + // for exponents or scale factors + if (index == start) return default_v; + + // type of multiplier determines type of result: + // multiplier is a float if we've seen digits past + // a decimal point, otherwise it's an int or long. + // Up to this point result is an int or long. + result *= multiplier; + + // now check for exponent or engineering scale factor. If there + // is one, result will be a float. + if (index < slen) { + var scale = s.charAt(index); + index += 1; + if (scale == 'e') { + var exponent = 0; + multiplier = 10.0; + if (index < slen) { + if (s.charAt(index) == '+') index += 1; + else if (s.charAt(index) == '-') { + index += 1; + multiplier = 0.1; + } + } + while (index < slen) { + if (s.charAt(index) >= '0' && s.charAt(index) <= '9') { + exponent = exponent*10 + ord(s.charAt(index)) - ord('0'); + index += 1; + } else break; + } + while (exponent > 0) { + exponent -= 1; + result *= multiplier; + } + } else if (scale == 't') result *= 1e12; + else if (scale == 'g') result *= 1e9; + else if (scale == 'k') result *= 1e3; + else if (scale == 'u') result *= 1e-6; + else if (scale == 'n') result *= 1e-9; + else if (scale == 'p') result *= 1e-12; + else if (scale == 'f') result *= 1e-15; + else if (scale == 'm') { + if (index+1 < slen) { + if (s.charAt(index) == 'e' && s.charAt(index+1) == 'g') + result *= 1e6; + else if (s.charAt(index) == 'i' && s.charAt(index+1) == 'l') + result *= 25.4e-6; + } else result *= 1e-3; + } else return default_v; + } + // ignore any remaining chars, eg, 1kohms returns 1000 + return result; + } + + /////////////////////////////////////////////////////////////////////////////// + // + // Sources + // + /////////////////////////////////////////////////////////////////////////////// + + // argument is a string describing the source's value: + // or dc() -- constant value + // pulse(,,,,,,) + // sin(,,,,) + // pwl(