diff --git a/js/cktsim.js b/js/cktsim.js index 8682a590b9..63d4ff74be 100644 --- a/js/cktsim.js +++ b/js/cktsim.js @@ -44,6 +44,7 @@ cktsim = (function() { this.devices = []; // list of devices this.device_map = new Array(); // map name -> device + this.voltage_sources = []; // list of voltage sources this.finalized = false; this.diddc = false; @@ -104,10 +105,14 @@ cktsim = (function() { var type = component[0]; // ignore wires, ground connections, scope probes and view info - if (type == 'view' || type == 'w' || type == 'g' || type == 's' || type == 'L') continue; + if (type == 'view' || type == 'w' || type == 'g' || type == 's' || type == 'L') { + continue; + } var properties = component[2]; var name = properties['name']; + if (name==undefined || name=='') + name = '_' + properties['_json_'].toString(); // convert node names to circuit indicies var connections = component[3]; @@ -134,11 +139,9 @@ cktsim = (function() { else if (type == 'o') // op amp this.opamp(connections[0],connections[1],connections[2],properties['A'],name); else if (type == 'n') // n fet - this.n(connections[0],connections[1],connections[2], - properties['W/L'],name); + this.n(connections[0],connections[1],connections[2],properties['W/L'],name); else if (type == 'p') // p fet - this.p(connections[0],connections[1],connections[2], - properties['W/L'],name); + this.p(connections[0],connections[1],connections[2],properties['W/L'],name); } } @@ -214,10 +217,16 @@ cktsim = (function() { this.diddc = true; // create solution dictionary var result = new Array(); + // capture node voltages for (var name in this.node_map) { var index = this.node_map[name]; result[name] = (index == -1) ? 0 : this.solution[index]; } + // capture branch currents from voltage sources + for (var i = this.voltage_sources.length - 1; i >= 0; --i) { + var v = this.voltage_sources[i]; + result['I('+v.name+')'] = this.solution[v.branch]; + } return result; } } @@ -457,6 +466,12 @@ cktsim = (function() { var index = this.node_map[name]; result[name] = (index == -1) ? 0 : response[index]; } + // capture branch currents from voltage sources + for (var i = this.voltage_sources.length - 1; i >= 0; --i) { + var v = this.voltage_sources[i]; + result['I('+v.name+')'] = response[v.branch]; + } + result['time'] = response[this.N]; return result; } @@ -537,6 +552,7 @@ cktsim = (function() { Circuit.prototype.add_device = function(d,name) { // Add device to list of devices and to device map this.devices.push(d); + d.name = name; if (name) { if (this.device_map[name] === undefined) this.device_map[name] = d; @@ -599,6 +615,7 @@ cktsim = (function() { Circuit.prototype.v = function(n1,n2,v,name) { var branch = this.node(undefined,T_CURRENT); var d = new VSource(n1,n2,branch,v); + this.voltage_sources.push(d); return this.add_device(d, name); } @@ -1029,9 +1046,11 @@ cktsim = (function() { /////////////////////////////////////////////////////////////////////////////// // argument is a string describing the source's value (see comments for details) - // source types: dc,step,square,triangle,sin,pulse,pwl,pwlr + // source types: dc,step,square,triangle,sin,pulse,pwl,pwl_repeating // returns an object with the following attributes: + // fun -- name of source function + // args -- list of argument values // value(t) -- compute source value at time t // inflection_point(t) -- compute time after t when a time point is needed // dc -- value at time 0 @@ -1071,32 +1090,35 @@ cktsim = (function() { // post-processing for constant sources // dc(v) if (src.fun == 'dc') { - var value = src.args[0]; - if (value === undefined) value = 0; - src.value = function(t) { return value; } // closure + var v = arg_value(src.args,0,0); + src.args = [v]; + src.value = function(t) { return v; } // closure } // post-processing for step sources - // step(v_init,v_plateau,t_delay,t_rise,t_fall) + // step(v_init,v_plateau,t_delay,t_rise) else if (src.fun == 'step') { var v1 = arg_value(src.args,0,0); // default init value: 0V var v2 = arg_value(src.args,1,1); // default plateau value: 1V var td = Math.max(0,arg_value(src.args,2,0)); // time step starts var tr = Math.abs(arg_value(src.args,3,1e-9)); // default rise time: 1ns + src.args = [v1,v2,td,tr]; // remember any defaulted values pwl_source(src,[td,v1,td+tr,v2],false); } // post-processing for square wave - // square(v_init,v_plateau,t_period) + // square(v_init,v_plateau,freq) else if (src.fun == 'square') { var v1 = arg_value(src.args,0,0); // default init value: 0V var v2 = arg_value(src.args,1,1); // default plateau value: 1V - var freq = Math.abs(arg_value(src.args,2,1)); // default frequency: 1s + var freq = Math.abs(arg_value(src.args,2,1)); // default frequency: 1Hz + src.args = [v1,v2,freq]; // remember any defaulted values var per = freq == 0 ? Infinity : 1/freq; var t_change = 0.01 * per; // rise and fall time var t_pw = 0.49 * per; // half the cycle minus rise and fall time - pwl_source(src,[0,v1,t_change,v2,t_change+t_pw,v2,t_change+t_pw+t_change,v1,per,v1],true); + pwl_source(src,[0,v1,t_change,v2,t_change+t_pw, + v2,t_change+t_pw+t_change,v1,per,v1],true); } // post-processing for triangle @@ -1105,6 +1127,7 @@ cktsim = (function() { var v1 = arg_value(src.args,0,0); // default init value: 0V var v2 = arg_value(src.args,1,1); // default plateau value: 1V var freq = Math.abs(arg_value(src.args,2,1)); // default frequency: 1s + src.args = [v1,v2,freq]; // remember any defaulted values var per = freq == 0 ? Infinity : 1/freq; pwl_source(src,[0,v1,per/2,v2,per,v1],true); @@ -1112,8 +1135,8 @@ cktsim = (function() { // post-processing for pwl and pwlr sources // pwl[r](t1,v1,t2,v2,...) - else if (src.fun == 'pwl' || src.fun == 'pwlr') { - pwl_source(src,src.args,src.fun == 'pwlr'); + else if (src.fun == 'pwl' || src.fun == 'pwl_repeating') { + pwl_source(src,src.args,src.fun == 'pwl_repeating'); } // post-processing for pulsed sources @@ -1122,18 +1145,18 @@ cktsim = (function() { var v1 = arg_value(src.args,0,0); // default init value: 0V var v2 = arg_value(src.args,1,1); // default plateau value: 1V var td = Math.max(0,arg_value(src.args,2,0)); // time pulse starts - var tr = Math.abs(arg_value(src.args,3,1e-9)); // default rise time: 1ns var tf = Math.abs(arg_value(src.args,4,1e-9)); // default rise time: 1ns var pw = Math.abs(arg_value(src.args,5,1e9)); // default pulse width: "infinite" var per = Math.abs(arg_value(src.args,6,1e9)); // default period: "infinite" + src.args = [v1,v2,td,tr,tf,pw,per]; var t1 = td; // time when v1 -> v2 transition starts var t2 = t1 + tr; // time when v1 -> v2 transition ends var t3 = t2 + pw; // time when v2 -> v1 transition starts var t4 = t3 + tf; // time when v2 -> v1 transition ends - pwl_source(src,[t1,v1,t2,v2,t3,v2,t4,v1,per,v1],true); + pwl_source(src,[t1,v1, t2,v2, t3,v2, t4,v1, per,v1],true); } // post-processing for sinusoidal sources @@ -1144,16 +1167,14 @@ cktsim = (function() { var freq = Math.abs(arg_value(src.args,2,1)); // default frequency: 1Hz var td = Math.max(0,arg_value(src.args,3,0)); // default time delay: 0sec var phase = arg_value(src.args,4,0); // default phase offset: 0 degrees + src.args = [voffset,va,freq,td,phase]; phase /= 360.0; // return value of source at time t src.value = function(t) { // closure if (t < td) return voffset + va*Math.sin(2*Math.PI*phase); - else { - var val = voffset + va*Math.sin(2*Math.PI*(freq*(t - td) + phase)); - return val; - } + else return voffset + va*Math.sin(2*Math.PI*(freq*(t - td) + phase)); } // return time of next inflection point after time t @@ -1163,9 +1184,6 @@ cktsim = (function() { } } - // to do: - // post-processing for piece-wise linear sources - // object has all the necessary info to compute the source value and inflection points src.dc = src.value(0); // DC value is value at time 0 return src; @@ -1531,6 +1549,7 @@ cktsim = (function() { var module = { 'Circuit': Circuit, 'parse_number': parse_number, + 'parse_source': parse_source, } return module; }()); diff --git a/js/schematic.js b/js/schematic.js index 211cdcdcae..61c931463d 100644 --- a/js/schematic.js +++ b/js/schematic.js @@ -594,8 +594,9 @@ schematic = (function() { var json = []; // output all the components/wires in the diagram - for (var i = this.components.length - 1; i >=0; --i) - json.push(this.components[i].json()); + var n = this.components.length; + for (var i = 0; i < n; i++) + json.push(this.components[i].json(i)); // capture the current view parameters json.push(['view',this.origin_x,this.origin_y,this.scale, @@ -932,6 +933,10 @@ schematic = (function() { // for each electrical node for (var location in this.connection_points) (this.connection_points[location])[0].display_voltage(c,temp); + + // let components display branch current info if available + for (var i = this.components.length - 1; i >= 0; --i) + this.components[i].display_current(c,temp) } } @@ -980,9 +985,6 @@ schematic = (function() { c.fillText(text,(x - this.origin_x) * this.scale,(y - this.origin_y) * this.scale); } - HTMLCanvasElement.prototype.totalOffset = function(){ - } - // add method to canvas to compute relative coords for event HTMLCanvasElement.prototype.relMouseCoords = function(event){ // run up the DOM tree to figure out coords for top,left of canvas @@ -1678,8 +1680,9 @@ schematic = (function() { return [vmin,vmax,1.0/scale]; } - function engineering_notation(n,nplaces) { + function engineering_notation(n,nplaces,trim) { if (n == 0) return("0"); + if (trim == undefined) trim = true; var sign = n < 0 ? -1 : 1; var log10 = Math.log(sign*n)/Math.LN10; @@ -1694,8 +1697,10 @@ schematic = (function() { if (nplaces > 0) { endindex += nplaces + 1; if (endindex > mlen) endindex = mlen; - while (mstring.charAt(endindex-1) == '0') endindex -= 1; - if (mstring.charAt(endindex-1) == '.') endindex -= 1; + if (trim) { + while (mstring.charAt(endindex-1) == '0') endindex -= 1; + if (mstring.charAt(endindex-1) == '.') endindex -= 1; + } } if (endindex < mlen) mstring = mstring.substring(0,endindex); @@ -1717,6 +1722,9 @@ schematic = (function() { return n.toString(); } + var grid_pattern = [1,2]; + var cursor_pattern = [5,5]; + // x_values is an array of x coordinates for each of the plots // y_values is an array of [color, value_array], one entry for each plot Schematic.prototype.graph = function(x_values,y_values,x_legend,y_legend) { @@ -1727,7 +1735,6 @@ schematic = (function() { var right_margin = 25; var bottom_margin = 45; var tick_length = 5; - var pattern = [1,2]; var w = pwidth + left_margin + right_margin; var h = pheight + top_margin + bottom_margin; @@ -1735,7 +1742,7 @@ schematic = (function() { var canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; - + // the graph itself will be drawn here and this image will be copied // onto canvas, where it can be overlayed with mouse cursors, etc. var bg_image = document.createElement('canvas'); @@ -1779,7 +1786,7 @@ schematic = (function() { c.moveTo(temp,top_margin); c.lineTo(temp,end); } else - c.dashedLineTo(temp,top_margin,temp,end,pattern); + c.dashedLineTo(temp,top_margin,temp,end,grid_pattern); c.stroke(); // tick mark @@ -1821,7 +1828,7 @@ schematic = (function() { c.moveTo(left_margin,temp); c.lineTo(left_margin + pwidth,temp); } else - c.dashedLineTo(left_margin,temp,left_margin + pwidth,temp,pattern); + c.dashedLineTo(left_margin,temp,left_margin + pwidth,temp,grid_pattern); c.stroke(); // tick mark @@ -1864,6 +1871,27 @@ schematic = (function() { c.fillText(y_legend,0,0); c.restore(); + // save info need for interactions with the graph + canvas.x_values = x_values; + canvas.y_values = y_values; + canvas.x_legend = x_legend; + canvas.y_legend = y_legend; + canvas.x_min = x_min; + canvas.x_scale = x_scale; + canvas.y_min = y_min; + canvas.y_scale = y_scale; + canvas.left_margin = left_margin; + canvas.top_margin = top_margin; + canvas.pwidth = pwidth; + canvas.pheight = pheight; + canvas.tick_length = tick_length; + + canvas.cursor_x = undefined; + canvas.sch = this; + + // do something useful when user mouses over graph + canvas.addEventListener('mousemove',graph_mouse_move,false); + // return our masterpiece redraw_plot(canvas); return canvas; @@ -1883,9 +1911,73 @@ schematic = (function() { return min; } - function redraw_plot(canvas) { - var c = canvas.getContext('2d'); - c.drawImage(canvas.bg_image,0,0); + function redraw_plot(graph) { + var c = graph.getContext('2d'); + c.drawImage(graph.bg_image,0,0); + + if (graph.cursor_x != undefined) { + // draw dashed vertical marker that follows mouse + var x = graph.left_margin + graph.cursor_x; + var end_y = graph.top_margin + graph.pheight + graph.tick_length; + c.strokeStyle = grid_style; + c.lineWidth = 1; + c.beginPath(); + c.dashedLineTo(x,graph.top_margin,x,end_y,cursor_pattern); + c.stroke(); + + // add x label at bottom of marker + var graph_x = graph.cursor_x/graph.x_scale + graph.x_min; + c.font = '10pt sans-serif'; + c.textAlign = 'center'; + c.textBaseline = 'top'; + c.fillStyle = background_style; + c.fillText('\u2588\u2588\u2588\u2588\u2588',x,end_y); + c.fillStyle = normal_style; + c.fillText(engineering_notation(graph_x,3,false),x,end_y); + + // compute which points marker is between + var x_values = graph.x_values; + var len = x_values.length; + var index = 0; + while (index < len && graph_x >= x_values[index]) index += 1; + var x1 = (index == 0) ? x_values[0] : x_values[index-1]; + var x2 = x_values[index]; + + // for each plot, interpolate and output value at intersection with marker + c.textAlign = 'left'; + var tx = graph.left_margin + 4; + var ty = graph.top_margin; + for (var plot = 0; plot < graph.y_values.length; plot++) { + var values = graph.y_values[plot][1]; + + // interpolate signal value at graph_x using values[index-1] and values[index] + var y1 = (index == 0) ? values[0] : values[index-1]; + var y2 = values[index]; + var y = y1; + if (graph_x != x1) y += (graph_x - x1)*(y2 - y1)/(x2 - x1); + + // annotate plot with value of signal at marker + c.fillStyle = element_style; + c.fillText('\u2588\u2588\u2588\u2588\u2588',tx-3,ty); + c.fillStyle = probe_colors_rgb[graph.y_values[plot][0]]; + c.fillText(engineering_notation(y,3,false),tx,ty); + ty += 14; + } + } + } + + function graph_mouse_move(event) { + if (!event) event = window.event; + var g = (window.event) ? event.srcElement : event.target; + + g.relMouseCoords(event); + // not sure yet where the 3,-3 offset correction comes from (borders? padding?) + var gx = g.mouse_x - g.left_margin - 3; + var gy = g.pheight - (g.mouse_y - g.top_margin) + 3; + if (gx >= 0 && gx <= g.pwidth && gy >=0 && gy <= g.pheight) g.cursor_x = gx; + else g.cursor_x = undefined; + + redraw_plot(g); } /////////////////////////////////////////////////////////////////////////////// @@ -2112,7 +2204,9 @@ schematic = (function() { this.connections = []; } - Component.prototype.json = function() { + Component.prototype.json = function(index) { + this.properties['_json_'] = index; // remember where we are in the JSON list + var props = {}; for (var p in this.properties) props[p] = this.properties[p]; @@ -2343,7 +2437,9 @@ schematic = (function() { // make an widget for each property var fields = new Array(); for (var i in this.properties) - fields[i] = build_input('text',10,this.properties[i]); + // underscore at beginning of property name => system property + if (i.charAt(0) != '_') + fields[i] = build_input('text',10,this.properties[i]); var content = build_table(fields); content.fields = fields; @@ -2383,6 +2479,10 @@ schematic = (function() { } } + // default behavior: nothing to display for DC analysis + Component.prototype.display_current = function(c,vmap) { + } + //////////////////////////////////////////////////////////////////////////////// // // Connection point @@ -2507,7 +2607,7 @@ schematic = (function() { return ''; } - Wire.prototype.json = function() { + Wire.prototype.json = function(index) { var json = ['w',[this.x, this.y, this.x+this.dx, this.y+this.dy]]; return json; } @@ -3024,14 +3124,18 @@ schematic = (function() { // //////////////////////////////////////////////////////////////////////////////// + function Source(x,y,rotation,name,type,value) { Component.call(this,type,x,y,rotation); this.properties['name'] = name; - this.properties['value'] = value ? value : '1'; + if (value == undefined) value = 'dc(1)'; + this.properties['value'] = value; this.add_connection(0,0); this.add_connection(0,48); this.bounding_box = [-12,0,12,48]; this.update_coords(); + + this.content = document.createElement('div'); // used by edit_properties } Source.prototype = new Component(); Source.prototype.constructor = Source; @@ -3068,10 +3172,141 @@ schematic = (function() { this.draw_text(c,this.properties['value'],13,24,3,property_size); } - Source.prototype.clone = function(x,y) { - return new Source(x,y,this.rotation,this.properties['name'],this.type,this.properties['value']); + // map source function name to labels for each source parameter + source_functions = { + 'dc': ['DC value'], + + 'step': ['Initial value', + 'Plateau value', + 'Delay until step (secs)', + 'Rise time (secs)'], + + 'square': ['Initial value', + 'Plateau value', + 'Frequency (Hz)'], + + 'triangle': ['Initial value', + 'Plateau value', + 'Frequency (Hz)'], + + 'pwl': ['Comma-separated list of alternating times and values'], + + 'pwl_repeating': ['Comma-separated list of alternating times and values'], + + 'pulse': ['Initial value', + 'Plateau value', + 'Delay until pulse (secs)', + 'Time for first transition (secs)', + 'Time for second transition (secs)', + 'Pulse width (secs)', + 'Period (secs)'], + + 'sin': ['Offset value', + 'Amplitude', + 'Frequency (Hz)', + 'Delay until sin starts (secs)', + 'Phase offset (degrees)'], } + // build property editor div + Source.prototype.build_content = function(src) { + // make an widget for each property + var fields = [] + fields['name'] = build_input('text',10,this.properties['name']); + + if (src == undefined) { + fields['value'] = this.properties['value']; + } else { + // fancy version: add select tag for source type + var src_types = []; + for (var t in source_functions) src_types.push(t); + var type_select = build_select(src_types,src.fun); + type_select.component = this; + type_select.addEventListener('change',source_type_changed,false) + fields['type'] = type_select; + + if (src.fun == 'pwl' || src.run == 'pwl_repeating') { + var v = ''; + var first = true; + for (var i = 0; i < src.args.length; i++) { + if (first) first = false; + else v += ','; + v += engineering_notation(src.args[i],3); + if (i % 2 == 0) v += 's'; + } + fields[source_functions[src.fun][0]] = build_input('text',30,v); + } else { + // followed separate input tag for each parameter + var labels = source_functions[src.fun]; + for (var i = 0; i < labels.length; i++) { + var v = engineering_notation(src.args[i],3); + fields[labels[i]] = build_input('text',10,v); + } + } + } + + var div = this.content; + if (div.hasChildNodes()) + div.removeChild(div.firstChild); // remove table of input fields + div.appendChild(build_table(fields)); + div.fields = fields; + div.component = this; + return div; + } + + function source_type_changed(event) { + if (!event) event = window.event; + var select = (window.event) ? event.srcElement : event.target; + + // see where to get source parameters from + var type = select.options[select.selectedIndex].value; + var src = undefined; + if (this.src != undefined && type == this.src.fun) + src = this.src; + else if (typeof cktsim != 'undefined') + src = cktsim.parse_source(type+'()'); + + select.component.build_content(src); + } + + Source.prototype.edit_properties = function(x,y) { + if (this.near(x,y)) { + this.src = undefined; + if (typeof cktsim != 'undefined') + this.src = cktsim.parse_source(this.properties['value']); + var content = this.build_content(this.src); + + this.sch.dialog('Edit Properties',content,function(content) { + var c = content.component; + var fields = content.fields; + + var first = true; + var value = ''; + for (var label in fields) { + if (label == 'name') + c.properties['name'] = fields['name'].value; + else if (label == 'value') { + // if unknown source type + value = fields['value'].value; + c.sch.redraw_background(); + return; + } else if (label == 'type') { + var select = fields['type']; + value = select.options[select.selectedIndex].value + '('; + } else { + if (first) first = false; + else value += ','; + value += fields[label].value; + } + } + c.properties['value'] = value + ')'; + c.sch.redraw_background(); + }); + return true; + } else return false; + } + + function VSource(x,y,rotation,name,value) { Source.call(this,x,y,rotation,name,'v',value); this.type = 'v'; @@ -3081,6 +3316,32 @@ schematic = (function() { VSource.prototype.toString = Source.prototype.toString; VSource.prototype.draw = Source.prototype.draw; VSource.prototype.clone = Source.prototype.clone; + VSource.prototype.build_content = Source.prototype.build_content; + VSource.prototype.edit_properties = Source.prototype.edit_properties; + + // display current for DC analysis + VSource.prototype.display_current = function(c,vmap) { + var name = this.properties['name']; + var label = 'I(' + (name ? name : '_' + this.properties['_json_']) + ')'; + var v = vmap[label]; + if (v != undefined) { + // first draw some solid blocks in the background + c.globalAlpha = 0.85; + this.draw_text(c,'\u2588\u2588\u2588',0,24,4,annotation_size,element_style); + c.globalAlpha = 1.0; + + // display the node voltage at this connection point + var i = engineering_notation(v,2) + 'A'; + this.draw_text(c,i,0,24,4,annotation_size,annotation_style); + + // only display each current once + delete vmap[label]; + } + } + + VSource.prototype.clone = function(x,y) { + return new VSource(x,y,this.rotation,this.properties['name'],this.properties['value']); + } function ISource(x,y,rotation,name,value) { Source.call(this,x,y,rotation,name,'i',value); @@ -3091,6 +3352,12 @@ schematic = (function() { ISource.prototype.toString = Source.prototype.toString; ISource.prototype.draw = Source.prototype.draw; ISource.prototype.clone = Source.prototype.clone; + ISource.prototype.build_content = Source.prototype.build_content; + ISource.prototype.edit_properties = Source.prototype.edit_properties; + + ISource.prototype.clone = function(x,y) { + return new ISource(x,y,this.rotation,this.properties['name'],this.properties['value']); + } /////////////////////////////////////////////////////////////////////////////// //