def __init__(self, *args, **kvargs): if 'toolkit' in kvargs: self.toolkit = kvargs['toolkit'] del kvargs['toolkit'] else: self.toolkit = default_toolkit self.nodenames = {} ## Add terminal nodes self.add_terminals(self.terminals) ## Set temporary terminal mapping information for use by instantiation ## method in higher hierarchy self.terminalhook = dict(zip(self.terminals, args)) ## Create instance parameters self.iparv = ParameterDict(*self.instparams) self.ipar = ParameterDict(*self.instparams) ## Subscribe to changes on ipar self.ipar.attach(self, updatemethod='_ipar_changed') ## set instance parameters from arguments self.ipar.set(**kvargs) ## Subscribe to updates of instance parameters if hasattr(self, 'update'): self.iparv.attach(self) self.update(self.ipar)
class Circuit(object): """Basic circuit class The circuit class models electric circuits but could be used for any conservative system. A circuit object contains a list of nodes and branches which are associated with node voltages and branch currents in the modelled system. Note that the values of these quantities are stored in separate analysis classes, never inside a circuit object. The nodes are connected to the outside through terminals. When the circuit is instanciated, the outside nodes are passed to the object via the terminals. **Attributes** *nodes* A list that contains Node objects. The first k nodes are terminal nodes where k is the number of terminals. *branches* list of Branch objects. The solver will solve for the currents through the branches. *terminals* list of terminal names *instparams* A list of valid instance parameters (Parameter objects) *ipar* A ParameterDict containing instance specific parameters *nodenames* A dictionary that maps a local node name to the node object in nodes. If the node is connnected to superior hierarchy levels through a terminal the terminal name must be the same as the local node name *terminalhook* Temporary storage of information about what nodes in the superior hierarchy level the terminals should be connected to during instantiation. It is a dictionary where the keys are terminal names and the values are node objects. The value is None when it is not used. The only reason for this attribute is to allow for bottom-up instantiations like: cir1['I1'] = R('n1', gnd) *linear* A boolean value that is true if i(x) and q(x) are linear functions """ nodes = [] branches = [] terminals = [] instparams = [] linear = True def __init__(self, *args, **kvargs): if 'toolkit' in kvargs: self.toolkit = kvargs['toolkit'] del kvargs['toolkit'] else: self.toolkit = default_toolkit self.nodenames = {} ## Add terminal nodes self.add_terminals(self.terminals) ## Set temporary terminal mapping information for use by instantiation ## method in higher hierarchy self.terminalhook = dict(zip(self.terminals, args)) ## Create instance parameters self.iparv = ParameterDict(*self.instparams) self.ipar = ParameterDict(*self.instparams) ## Subscribe to changes on ipar self.ipar.attach(self, updatemethod='_ipar_changed') ## set instance parameters from arguments self.ipar.set(**kvargs) ## Subscribe to updates of instance parameters if hasattr(self, 'update'): self.iparv.attach(self) self.update(self.ipar) def __eq__(self, a): return self.__class__ == a.__class__ and \ self.nodes == a.nodes and \ self.nodenames == a.nodenames and self.branches == a.branches and \ self.iparv == a.iparv def __copy__(self): newc = self.__class__() newc.toolkit = self.toolkit newc.nodes = copy(self.nodes) newc.nodenames = copy(self.nodenames) newc.branches = copy(self.branches) newc.instparams = copy(self.instparams) newc.ipar = copy(self.ipar) newc.ipar.detach(self) newc.ipar.attach(newc, updatemethod='_ipar_changed') newc.iparv = copy(self.iparv) if hasattr(newc, 'update'): newc.iparv.detach(self) newc.iparv.attach(newc) newc.update(newc.ipar) newc.linear = copy(self.linear) newc.terminals = copy(self.terminals) return newc def _ipar_changed(self, subject): self.update_iparv(ignore_errors=True) def add_nodes(self, *names): """Create internal nodes in the circuit and return the new nodes >>> c = Circuit() >>> n1, n2 = c.add_nodes("n1", "n2") >>> c.nodes [Node('n1'), Node('n2')] >>> 'n1' in c.nodenames and 'n2' in c.nodenames True """ newnodes = [] for name in names: newnodes.append(Node(name)) self.append_node(newnodes[-1]) return tuple(newnodes) def add_node(self, name): """Create and internal node in the circuit and return the new node""" return self.add_nodes(name)[0] def append_node(self, node): """Append node object to circuit""" ## Make a copy of node list so the class is unchanged if self.__class__.nodes is self.nodes: self.nodes = list(self.nodes) if node not in self.nodes: self.nodes.append(node) self.nodenames[node.name] = node def append_branches(self, *branches): """Append node object to circuit""" ## Make a copy of branch list so the class is unchanged if self.__class__.branches is self.branches: self.branches = list(self.branches) self.branches.extend(branches) def get_terminal_branch(self, terminalname): """Find the branch that is connected to the given terminal If no branch is found or if there are more branches than one, None is returned Returns ------- Tuple of Branch object and an integer indicating if terminal is connected to the positive or negative side of the branch. 1 == positive side, -1 == negative side >>> from elements import * >>> net1 = Node('net1') >>> net2 = Node('net2') >>> VS = VS(net1, net2) >>> VS.get_terminal_branch("minus") (Branch(Node('plus'),Node('minus')), -1) """ plusbranches = [] ## Branches with positive side connected to the ## terminal minusbranches = [] ## Branches with negative side connected to the ## terminal for branch in self.branches: if branch.plus == self.nodenames[terminalname]: plusbranches.append(branch) elif branch.minus == self.nodenames[terminalname]: minusbranches.append(branch) if len(plusbranches + minusbranches) != 1: return None elif len(plusbranches) == 1: return plusbranches[0], 1 elif len(minusbranches) == 1: return minusbranches[0], -1 def get_node_index(self, node, refnode=None): """Get row in the x vector of a node instance If the refnode argument is given the reference node is assumed to be removed """ if not isinstance(node, Node): node = Node(str(node)) if refnode and not isinstance(refnode, Node): refnode = Node(str(refnode)) if node in self.nodes: index = self.nodes.index(node) if refnode != None: irefnode = self.nodes.index(refnode) if index == irefnode: return None if index > irefnode: return index - 1 return index else: raise ValueError('Node %s is not in circuit node list (%s)'% (str(node), str(self.nodes))) def get_branch_index(self, branch): """Get row in the x vector of a branch instance""" if branch in self.branches: return len(self.nodes) + self.branches.index(branch) else: raise ValueError('Branch %s is not present in circuit (%s)'% (str(branch), str(self.branches))) def get_node(self, name): """Find a node by name. >>> c = Circuit() >>> n1 = c.add_node("n1") >>> c.get_node('n1') Node('n1') """ return self.nodenames[name] def get_node_name(self, node): """Find the name of a node >>> c = Circuit() >>> n1 = c.add_node("n1") >>> c.get_node_name(n1) 'n1' """ for k, v in self.nodenames.items(): if v == node: return k def add_terminals(self, terminals): """Add terminals to circuit >>> c = Circuit() >>> c.add_terminals(["n1"]) >>> c.terminals ['n1'] """ if self.__class__.terminals is self.terminals: self.terminals = list(self.terminals) for terminal in terminals: # add terminal to terminal list if it is not included if terminal not in self.terminals: self.terminals.append(terminal) ## If no node with terminal name exists create node if not self.nodenames.has_key(terminal): self.add_node(terminal) node = self.nodenames[terminal] ## move node to position k in nodes as it is ## now a terminal node if not self.nodes.index(node) < self._nterminalnodes: self.nodes.remove(node) self.nodes.insert(self._nterminalnodes-1, node) def connect_terminals(self, **kvargs): """Connect nodes to terminals by using keyword arguments """ for terminal, node in kvargs.items(): ## Sanity check if type(terminal) is not types.StringType: raise Exception("%s should be string"%str(terminal)) if terminal not in self.terminals: raise ValueError('terminal '+str(terminal)+' is not defined') if not isinstance(node, Node): node = Node(str(node)) if terminal in self.nodenames: oldterminalnode = self.nodenames[terminal] if oldterminalnode != node: self.nodes.remove(self.nodenames[terminal]) if node not in self.nodes: self.nodes.insert(self._nterminalnodes, node) self.nodenames[terminal] = node def save_current(self, terminal): """Returns a circuit where a current probe is added at a terminal >>> from elements import * >>> import numpy as np >>> cir = R(Node('n1'), gnd, r=1e3) >>> newcir = cir.save_current('plus') >>> newcir.G(np.zeros(4)) array([[ 0. , 0. , 0. , 1. ], [ 0. , 0.001, -0.001, 0. ], [ 0. , -0.001, 0.001, -1. ], [ 1. , 0. , -1. , 0. ]]) """ if self.get_terminal_branch(terminal) == None: return ProbeWrapper(self, terminals = (terminal,)) else: return self @property def n(self): """Return size of x vector""" return len(self.nodes) + len(self.branches) def terminal_nodes(self): """Return a list of all terminal nodes""" return self.nodes[0:self._nterminalnodes] def non_terminal_nodes(self, instancename = None): """Return a list of all non-terminal nodes. If the instancename is set, the local nodes will have a instancename<dot> prefix added to the node name """ if instancename == None: return self.nodes[self._nterminalnodes:] else: result = [] for node in self.nodes[len(self.terminals):]: if node.isglobal: result.append(node) else: result.append(Node(instancename + '.' + node.name)) return result def G(self, x, epar=defaultepar): """Calculate the G (trans)conductance matrix given the x-vector""" return self.toolkit.zeros((self.n, self.n)) def C(self, x, epar=defaultepar): """Calculate the C (transcapacitance) matrix given the x-vector""" return self.toolkit.zeros((self.n, self.n)) def u(self, t=0.0, epar=defaultepar, analysis=None): """Calculate the u column-vector of the circuit at time t Arguments --------- epar -- ParameterDict with environment parameters such as temperature analysis -- This argument gives the possibility to have analysis dependent sources. for normal time dependent and dc sources this argument should be None """ return self.toolkit.zeros(self.n) def i(self, x, epar=defaultepar): """Calculate the i vector as a function of the x-vector For linear circuits i(x(t)) = G*x """ return self.toolkit.dot(self.G(x), x) def q(self, x, epar=defaultepar): """Calculate the q vector as a function of the x-vector For linear circuits q(x(t)) = C*x """ return self.toolkit.dot(self.C(x), x) def CY(self, x, w, epar=defaultepar): """Calculate the noise sources correlation matrix Arguments --------- x -- (numpy array) the state vector w -- Angular frequency epar -- (ParameterDict) Environment parameters """ return self.toolkit.zeros((self.n, self.n)) def next_event(self, t): """Returns the time of the next event given the current time t""" return inf def name_state_vector(self, x, analysis=''): """Return a dictionary of the x-vector keyed by node and branch names >>> from elements import * >>> import numpy as np >>> c = SubCircuit() >>> n1 = c.add_node('net1') >>> c['is'] = IS(gnd, n1, i=1e-3) >>> c['R'] = R(n1, gnd, r=1e3) >>> c.name_state_vector(np.array([[1.0]])) {'net1': 1.0} >>> """ result = {} for xvalue, node in zip(x[:len(self.nodes)][0], self.nodes): result[self.get_node_name(node)] = xvalue nnodes = len(self.nodes) for i, xvalue, branch in enumerate(zip(x[nnodes:], self.branches)): result['i' + analysis + str(i) + ')'] = xvalue return result def stamp_v(self, x, value, nodep, noden=None, refnode=None): """Stamp value in vector such that x[nodep] += value, x[noden] -= value If refnode is not None the reference node is assumed to be removed from vector """ x[self.get_node_index(nodep, refnode)] += value x[self.get_node_index(noden, refnode)] -= value def remove_refnode(self, matrices, refnode): """Remove refnode from vectors or matrices""" n = self.get_node_index(refnode) result = [] for A in matrices: for axis in range(len(A.shape)): A=self.toolkit.delete(A, [n], axis=axis) result.append(A) return tuple(result) def extract_v(self, x, nodep, noden=None, refnode=gnd, refnode_removed=False): """Extract voltage between nodep and noden from the given x-vector. If noden is not given the voltage is taken between nodep and refnode. x-vectors with the reference node removed can be handled by setting the refnode_removed to True. *x* x-vector *nodep* Node object or node reference in text format of positive node *noden* Node object or node reference in text format of negative node *refnode* reference node *refnode_removed* If set the refernce node is expected to be removed from the x-vector >>> from elements import * >>> import numpy as np >>> c = SubCircuit() >>> n1, n2 = c.add_nodes('n1','n2') >>> c['R1'] = R(n1, n2, r=1e3) >>> c['R2'] = R(n2, gnd, r=1e3) >>> c.extract_v(np.array([1.0, 0.5, 0.0]), 'n1', 'n2') 0.5 >>> c.extract_v(np.array([1.0, 0.5, 0.0]), c.nodes[0]) 1.0 >>> c.extract_v(np.array([1.0, 0.5]), c.nodes[0], refnode_removed = True) 1.0 """ v = [] for node in nodep, noden: if type(node) is types.StringType: node = self.get_node(node) elif node == None: node = refnode if refnode_removed: nodeindex = self.get_node_index(node, refnode) else: nodeindex = self.get_node_index(node, None) if nodeindex == None: ## When node == refnode v.append(0) continue v.append(x[nodeindex]) return v[0] - v[1] def extract_i(self, x, branch_or_term, xdot = None, refnode = gnd, refnode_removed = False, t = 0, linearized = False, xdcop = None): """Extract branch or terminal current from the given x-vector. *x* x-vector *branch_or_term* Branch object or terminal name *xdot* dx/dt vector. this is needed if dx/dt is non-zero and there is no branch defined at the terminal *refnode* reference node *refnode_removed* If set the refernce node is expected to be removed from the x-vector *t* Time when the sources are to be evaluated *linearized* Set to True if the AC current is wanted *xdcop* *xcdop* is the DC operation point x-vector if linearized == True >>> from elements import * >>> import numpy as np >>> c = SubCircuit() >>> net1 = c.add_node('net1') >>> c['vs'] = VS(net1, gnd) >>> c.extract_i(np.array([1.0, 0, -1e-3]), 'vs.minus') 0.001 >>> c.extract_i(np.array([1.0, -1e-3]), 'vs.minus', refnode_removed = True) 0.001 """ dot = self.toolkit.dot if type(branch_or_term) is types.StringType: ## Calculate current going in to the terminal as ## self.i(x)[terminal_node] + u(t) + dq(x)/dt. ## This will work since i(x) returns ## the sum of all the currents going out from the ## terminal node that originates from devices within ## the circuit. According to Kirchoff's current law ## of the terminal node ## -I_external + sum(I_internal_k) = 0 ## Where I_external represents the current coming from ## outside the circuit going *in* to the terminal node, ## I_internal_k represents one of the currents that flows ## from the terminal node to a device within the circuit. ## So now we can calculate I_external as ## I_external = sum(I_internal_k) = ## self.I(x)[terminal_node] + u(t) + dq(x)/dt = ## self.I(x)[terminal_node] + u(t) + sum(dq(x)/dx_k * dx_k/dt) = ## self.I(x)[terminal_node] + u(t) + C(x) * dx/dt branch_sign = self.get_terminal_branch(branch_or_term) if branch_sign != None: branch, sign = branch_sign else: terminal_node = self.nodenames[branch_or_term] terminal_node_index = self.get_node_index(terminal_node) if xdot != None: if linearized: return dot(self.G(xdcop)[terminal_node_index], x) + \ dot(self.C(xdcop)[terminal_node_index], xdot) + \ self.u(t, analysis = 'ac')[terminal_node_index] else: return self.i(x)[terminal_node_index] + \ dot(self.C(x)[terminal_node_index], xdot) + \ self.u(t)[terminal_node_index] else: if linearized: return dot(self.G(xdcop)[terminal_node_index], x) + \ self.u(t, analysis = 'ac')[terminal_node_index] else: return self.i(x)[terminal_node_index] + \ self.u(t)[terminal_node_index] else: branch = branch_or_term sign = 1 branchindex = self.get_branch_index(branch) if refnode_removed: branchindex -= 1 return sign * x[branchindex] def update_iparv(self, parent_ipar=None, globalparams=None, ignore_errors=False): """Calculate numeric values of instance parameters""" substvalues = tuple(p for p in (globalparams, parent_ipar) if p) newipar = self.ipar.eval_expressions(substvalues, ignore_errors=ignore_errors) self.iparv.update_values(newipar) def __repr__(self): return self.__class__.__name__ + \ '(' + \ ','.join([repr(self.nodenames[term].name) for term in self.terminals] + ['%s=%s'%(par.name, self.ipar.get(par)) for par in self.ipar.parameters]) + ')' def _instance_nodes(self, instancenodes, instance, instancename): """Return circuit nodes from instance nodes """ for instancenode in instancenodes: if instancenode.isglobal: yield instancenode elif instancenode.name in instance.terminals: terminal = instancenode.name yield self.term_node_map[instancename][terminal] else: yield Node(instancename + '.' + instancenode.name) def _instance_branches(self, instance, instancename, instancebranches = None): """Return circuit branches from instance branches """ if instancebranches == None: instancebranches = instance.branches for instancebranch in instancebranches: plus, minus = self._instance_nodes([instancebranch.plus, instancebranch.minus], instance, instancename) yield Branch(plus,minus) @property def _nterminalnodes(self): """Return number of terminal nodes""" return len(self.terminals)
class Circuit(object): """Basic circuit class The circuit class models electric circuits but could be used for any conservative system. A circuit object contains a list of nodes and branches which are associated with node voltages and branch currents in the modelled system. Note that the values of these quantities are stored in separate analysis classes, never inside a circuit object. The nodes are connected to the outside through terminals. When the circuit is instanciated, the outside nodes are passed to the object via the terminals. **Attributes** *nodes* A list that contains Node objects. The first k nodes are terminal nodes where k is the number of terminals. *branches* list of Branch objects. The solver will solve for the currents through the branches. *terminals* list of terminal names *instparams* A list of valid instance parameters (Parameter objects) *ipar* A ParameterDict containing instance specific parameters *nodenames* A dictionary that maps a local node name to the node object in nodes. If the node is connnected to superior hierarchy levels through a terminal the terminal name must be the same as the local node name *terminalhook* Temporary storage of information about what nodes in the superior hierarchy level the terminals should be connected to during instantiation. It is a dictionary where the keys are terminal names and the values are node objects. The value is None when it is not used. The only reason for this attribute is to allow for bottom-up instantiations like: cir1['I1'] = R('n1', gnd) *linear* A boolean value that is true if i(x) and q(x) are linear functions """ nodes = [] branches = [] terminals = [] instparams = [] linear = True def __init__(self, *args, **kvargs): if 'toolkit' in kvargs: self.toolkit = kvargs['toolkit'] del kvargs['toolkit'] else: self.toolkit = default_toolkit self.nodenames = {} ## Add terminal nodes self.add_terminals(self.terminals) ## Set temporary terminal mapping information for use by instantiation ## method in higher hierarchy self.terminalhook = dict(zip(self.terminals, args)) ## Create instance parameters self.iparv = ParameterDict(*self.instparams) self.ipar = ParameterDict(*self.instparams) ## Subscribe to changes on ipar self.ipar.attach(self, updatemethod='_ipar_changed') ## set instance parameters from arguments self.ipar.set(**kvargs) ## Subscribe to updates of instance parameters if hasattr(self, 'update'): self.iparv.attach(self) self.update(self.ipar) def __eq__(self, a): return self.__class__ == a.__class__ and \ self.nodes == a.nodes and \ self.nodenames == a.nodenames and self.branches == a.branches and \ self.iparv == a.iparv def __copy__(self): newc = self.__class__() newc.toolkit = self.toolkit newc.nodes = copy(self.nodes) newc.nodenames = copy(self.nodenames) newc.branches = copy(self.branches) newc.instparams = copy(self.instparams) newc.ipar = copy(self.ipar) newc.ipar.detach(self) newc.ipar.attach(newc, updatemethod='_ipar_changed') newc.iparv = copy(self.iparv) if hasattr(newc, 'update'): newc.iparv.detach(self) newc.iparv.attach(newc) newc.update(newc.ipar) newc.linear = copy(self.linear) newc.terminals = copy(self.terminals) return newc def _ipar_changed(self, subject): self.update_iparv(ignore_errors=True) def add_nodes(self, *names): """Create internal nodes in the circuit and return the new nodes >>> c = Circuit() >>> n1, n2 = c.add_nodes("n1", "n2") >>> c.nodes [Node('n1'), Node('n2')] >>> 'n1' in c.nodenames and 'n2' in c.nodenames True """ newnodes = [] for name in names: newnodes.append(Node(name)) self.append_node(newnodes[-1]) return tuple(newnodes) def add_node(self, name): """Create and internal node in the circuit and return the new node""" return self.add_nodes(name)[0] def append_node(self, node): """Append node object to circuit""" ## Make a copy of node list so the class is unchanged if self.__class__.nodes is self.nodes: self.nodes = list(self.nodes) if node not in self.nodes: self.nodes.append(node) self.nodenames[node.name] = node def append_branches(self, *branches): """Append node object to circuit""" ## Make a copy of branch list so the class is unchanged if self.__class__.branches is self.branches: self.branches = list(self.branches) self.branches.extend(branches) def get_terminal_branch(self, terminalname): """Find the branch that is connected to the given terminal If no branch is found or if there are more branches than one, None is returned Returns ------- Tuple of Branch object and an integer indicating if terminal is connected to the positive or negative side of the branch. 1 == positive side, -1 == negative side >>> from elements import * >>> net1 = Node('net1') >>> net2 = Node('net2') >>> VS = VS(net1, net2) >>> VS.get_terminal_branch("minus") (Branch(Node('plus'),Node('minus')), -1) """ plusbranches = [] ## Branches with positive side connected to the ## terminal minusbranches = [] ## Branches with negative side connected to the ## terminal for branch in self.branches: if branch.plus == self.nodenames[terminalname]: plusbranches.append(branch) elif branch.minus == self.nodenames[terminalname]: minusbranches.append(branch) if len(plusbranches + minusbranches) != 1: return None elif len(plusbranches) == 1: return plusbranches[0], 1 elif len(minusbranches) == 1: return minusbranches[0], -1 def get_node_index(self, node, refnode=None): """Get row in the x vector of a node instance If the refnode argument is given the reference node is assumed to be removed """ if not isinstance(node, Node): node = Node(str(node)) if refnode and not isinstance(refnode, Node): refnode = Node(str(refnode)) if node in self.nodes: index = self.nodes.index(node) if refnode is not None: irefnode = self.nodes.index(refnode) if index == irefnode: return None if index > irefnode: return index - 1 return index else: raise ValueError('Node %s is not in circuit node list (%s)' % (str(node), str(self.nodes))) def get_branch_index(self, branch): """Get row in the x vector of a branch instance""" if branch in self.branches: return len(self.nodes) + self.branches.index(branch) else: raise ValueError('Branch %s is not present in circuit (%s)' % (str(branch), str(self.branches))) def get_node(self, name): """Find a node by name. >>> c = Circuit() >>> n1 = c.add_node("n1") >>> c.get_node('n1') Node('n1') """ return self.nodenames[name] def get_node_name(self, node): """Find the name of a node >>> c = Circuit() >>> n1 = c.add_node("n1") >>> c.get_node_name(n1) 'n1' """ for k, v in self.nodenames.items(): if v == node: return k def add_terminals(self, terminals): """Add terminals to circuit >>> c = Circuit() >>> c.add_terminals(["n1"]) >>> c.terminals ['n1'] """ if self.__class__.terminals is self.terminals: self.terminals = list(self.terminals) for terminal in terminals: # add terminal to terminal list if it is not included if terminal not in self.terminals: self.terminals.append(terminal) ## If no node with terminal name exists create node if not self.nodenames.has_key(terminal): self.add_node(terminal) node = self.nodenames[terminal] ## move node to position k in nodes as it is ## now a terminal node if not self.nodes.index(node) < self._nterminalnodes: self.nodes.remove(node) self.nodes.insert(self._nterminalnodes - 1, node) def connect_terminals(self, **kvargs): """Connect nodes to terminals by using keyword arguments """ for terminal, node in kvargs.items(): ## Sanity check if type(terminal) is not types.StringType: raise Exception("%s should be string" % str(terminal)) if terminal not in self.terminals: raise ValueError('terminal ' + str(terminal) + ' is not defined') if not isinstance(node, Node): node = Node(str(node)) if terminal in self.nodenames: oldterminalnode = self.nodenames[terminal] if oldterminalnode != node: self.nodes.remove(self.nodenames[terminal]) if node not in self.nodes: self.nodes.insert(self._nterminalnodes, node) self.nodenames[terminal] = node def save_current(self, terminal): """Returns a circuit where a current probe is added at a terminal >>> from elements import * >>> import numpy as np >>> cir = R(Node('n1'), gnd, r=1e3) >>> newcir = cir.save_current('plus') >>> newcir.G(np.zeros(4)) array([[ 0. , 0. , 0. , 1. ], [ 0. , 0.001, -0.001, 0. ], [ 0. , -0.001, 0.001, -1. ], [ 1. , 0. , -1. , 0. ]]) """ if self.get_terminal_branch(terminal) is None: return ProbeWrapper(self, terminals=(terminal, )) else: return self @property def n(self): """Return size of x vector""" return len(self.nodes) + len(self.branches) def terminal_nodes(self): """Return a list of all terminal nodes""" return self.nodes[0:self._nterminalnodes] def non_terminal_nodes(self, instancename=None): """Return a list of all non-terminal nodes. If the instancename is set, the local nodes will have a instancename<dot> prefix added to the node name """ if instancename is None: return self.nodes[self._nterminalnodes:] else: result = [] for node in self.nodes[len(self.terminals):]: if node.isglobal: result.append(node) else: result.append(Node(instancename + '.' + node.name)) return result def G(self, x, epar=defaultepar): """Calculate the G (trans)conductance matrix given the x-vector""" return self.toolkit.zeros((self.n, self.n)) def C(self, x, epar=defaultepar): """Calculate the C (transcapacitance) matrix given the x-vector""" return self.toolkit.zeros((self.n, self.n)) def u(self, t=0.0, epar=defaultepar, analysis=None): """Calculate the u column-vector of the circuit at time t Arguments --------- epar -- ParameterDict with environment parameters such as temperature analysis -- This argument gives the possibility to have analysis dependent sources. for normal time dependent and dc sources this argument should be None """ return self.toolkit.zeros(self.n) def i(self, x, epar=defaultepar): """Calculate the i vector as a function of the x-vector For linear circuits i(x(t)) = G*x """ return self.toolkit.dot(self.G(x), x) def q(self, x, epar=defaultepar): """Calculate the q vector as a function of the x-vector For linear circuits q(x(t)) = C*x """ return self.toolkit.dot(self.C(x), x) def CY(self, x, w, epar=defaultepar): """Calculate the noise sources correlation matrix Arguments --------- x -- (numpy array) the state vector w -- Angular frequency epar -- (ParameterDict) Environment parameters """ return self.toolkit.zeros((self.n, self.n)) def next_event(self, t): """Returns the time of the next event given the current time t""" return inf def name_state_vector(self, x, analysis=''): """Return a dictionary of the x-vector keyed by node and branch names >>> from elements import * >>> import numpy as np >>> c = SubCircuit() >>> n1 = c.add_node('net1') >>> c['is'] = IS(gnd, n1, i=1e-3) >>> c['R'] = R(n1, gnd, r=1e3) >>> c.name_state_vector(np.array([[1.0]])) {'net1': 1.0} >>> """ result = {} for xvalue, node in zip(x[:len(self.nodes)][0], self.nodes): result[self.get_node_name(node)] = xvalue nnodes = len(self.nodes) for i, xvalue, branch in enumerate(zip(x[nnodes:], self.branches)): result['i' + analysis + str(i) + ')'] = xvalue return result def stamp_v(self, x, value, nodep, noden=None, refnode=None): """Stamp value in vector such that x[nodep] += value, x[noden] -= value If refnode is not None the reference node is assumed to be removed from vector """ x[self.get_node_index(nodep, refnode)] += value x[self.get_node_index(noden, refnode)] -= value def remove_refnode(self, matrices, refnode): """Remove refnode from vectors or matrices""" n = self.get_node_index(refnode) result = [] for A in matrices: for axis in range(len(A.shape)): A = self.toolkit.delete(A, [n], axis=axis) result.append(A) return tuple(result) def extract_v(self, x, nodep, noden=None, refnode=gnd, refnode_removed=False): """Extract voltage between nodep and noden from the given x-vector. If noden is not given the voltage is taken between nodep and refnode. x-vectors with the reference node removed can be handled by setting the refnode_removed to True. *x* x-vector *nodep* Node object or node reference in text format of positive node *noden* Node object or node reference in text format of negative node *refnode* reference node *refnode_removed* If set the refernce node is expected to be removed from the x-vector >>> from elements import * >>> import numpy as np >>> c = SubCircuit() >>> n1, n2 = c.add_nodes('n1','n2') >>> c['R1'] = R(n1, n2, r=1e3) >>> c['R2'] = R(n2, gnd, r=1e3) >>> c.extract_v(np.array([1.0, 0.5, 0.0]), 'n1', 'n2') 0.5 >>> c.extract_v(np.array([1.0, 0.5, 0.0]), c.nodes[0]) 1.0 >>> c.extract_v(np.array([1.0, 0.5]), c.nodes[0], refnode_removed = True) 1.0 """ v = [] for node in nodep, noden: if type(node) is types.StringType: node = self.get_node(node) elif node is None: node = refnode if refnode_removed: nodeindex = self.get_node_index(node, refnode) else: nodeindex = self.get_node_index(node, None) if nodeindex is None: ## When node == refnode v.append(0) continue v.append(x[nodeindex]) return v[0] - v[1] def extract_i(self, x, branch_or_term, xdot=None, refnode=gnd, refnode_removed=False, t=0, linearized=False, xdcop=None): """Extract branch or terminal current from the given x-vector. *x* x-vector *branch_or_term* Branch object or terminal name *xdot* dx/dt vector. this is needed if dx/dt is non-zero and there is no branch defined at the terminal *refnode* reference node *refnode_removed* If set the refernce node is expected to be removed from the x-vector *t* Time when the sources are to be evaluated *linearized* Set to True if the AC current is wanted *xdcop* *xcdop* is the DC operation point x-vector if linearized == True >>> from elements import * >>> import numpy as np >>> c = SubCircuit() >>> net1 = c.add_node('net1') >>> c['vs'] = VS(net1, gnd) >>> c.extract_i(np.array([1.0, 0, -1e-3]), 'vs.minus') 0.001 >>> c.extract_i(np.array([1.0, -1e-3]), 'vs.minus', refnode_removed = True) 0.001 """ dot = self.toolkit.dot if type(branch_or_term) is types.StringType: ## Calculate current going in to the terminal as ## self.i(x)[terminal_node] + u(t) + dq(x)/dt. ## This will work since i(x) returns ## the sum of all the currents going out from the ## terminal node that originates from devices within ## the circuit. According to Kirchoff's current law ## of the terminal node ## -I_external + sum(I_internal_k) = 0 ## Where I_external represents the current coming from ## outside the circuit going *in* to the terminal node, ## I_internal_k represents one of the currents that flows ## from the terminal node to a device within the circuit. ## So now we can calculate I_external as ## I_external = sum(I_internal_k) = ## self.I(x)[terminal_node] + u(t) + dq(x)/dt = ## self.I(x)[terminal_node] + u(t) + sum(dq(x)/dx_k * dx_k/dt) = ## self.I(x)[terminal_node] + u(t) + C(x) * dx/dt branch_sign = self.get_terminal_branch(branch_or_term) if branch_sign is not None: branch, sign = branch_sign else: terminal_node = self.nodenames[branch_or_term] terminal_node_index = self.get_node_index(terminal_node) if xdot is not None: if linearized: return dot(self.G(xdcop)[terminal_node_index], x) + \ dot(self.C(xdcop)[terminal_node_index], xdot) + \ self.u(t, analysis = 'ac')[terminal_node_index] else: return self.i(x)[terminal_node_index] + \ dot(self.C(x)[terminal_node_index], xdot) + \ self.u(t)[terminal_node_index] else: if linearized: return dot(self.G(xdcop)[terminal_node_index], x) + \ self.u(t, analysis = 'ac')[terminal_node_index] else: return self.i(x)[terminal_node_index] + \ self.u(t)[terminal_node_index] else: branch = branch_or_term sign = 1 branchindex = self.get_branch_index(branch) if refnode_removed: branchindex -= 1 return sign * x[branchindex] def update_iparv(self, parent_ipar=None, globalparams=None, ignore_errors=False): """Calculate numeric values of instance parameters""" substvalues = tuple(p for p in (globalparams, parent_ipar) if p) newipar = self.ipar.eval_expressions(substvalues, ignore_errors=ignore_errors) self.iparv.update_values(newipar) def __repr__(self): return self.__class__.__name__ + \ '(' + \ ','.join([repr(self.nodenames[term].name) for term in self.terminals] + ['%s=%s'%(par.name, self.ipar.get(par)) for par in self.ipar.parameters]) + ')' def _instance_nodes(self, instancenodes, instance, instancename): """Return circuit nodes from instance nodes """ for instancenode in instancenodes: if instancenode.isglobal: yield instancenode elif instancenode.name in instance.terminals: terminal = instancenode.name yield self.term_node_map[instancename][terminal] else: yield Node(instancename + '.' + instancenode.name) def _instance_branches(self, instance, instancename, instancebranches=None): """Return circuit branches from instance branches """ if instancebranches is None: instancebranches = instance.branches for instancebranch in instancebranches: plus, minus = self._instance_nodes( [instancebranch.plus, instancebranch.minus], instance, instancename) yield Branch(plus, minus) @property def _nterminalnodes(self): """Return number of terminal nodes""" return len(self.terminals)
@property def V(self): return Quantity('V', self) @property def I(self): return Quantity('I', self) def __repr__(self): return 'Branch(' + repr(self.plus) + ',' + repr(self.minus) + ')' ### Default reference node gnd = Node("gnd", isglobal=True) defaultepar = ParameterDict( Parameter("T", "Temperature", unit="K", default=300)) class Circuit(object): """Basic circuit class The circuit class models electric circuits but could be used for any conservative system. A circuit object contains a list of nodes and branches which are associated with node voltages and branch currents in the modelled system. Note that the values of these quantities are stored in separate analysis classes, never inside a circuit object. The nodes are connected to the outside through terminals. When the circuit is instanciated, the outside nodes are passed to the object via the terminals.
def __init__(self, sim, **parvalues): self.sim = sim self.par = ParameterDict(*self.parameters, **parvalues)