Source code for ahkab.circuit

# -*- coding: iso-8859-1 -*-
# circuit.py
# Describes the circuit
# Copyright 2006 Giuseppe Venturini

# This file is part of the ahkab simulator.
#
# Ahkab is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 2 of the License.
#
# Ahkab is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License v2
# along with ahkab.  If not, see <http://www.gnu.org/licenses/>.

"""
Introduction
\"\"\"\"\"\"\"\"\"\"\"\"

A circuit is described in the ahkab simulator by an instance of the
:class:`Circuit` class.

This class holds everything is needed to simulate the circuit, except
the specification of the analyses to be performed.

To rewrite a netlist from a Circuit instance see the
:mod:`ahkab.printing` module.

The Circuit
\"\"\"\"\"\"\"\"\"\"\"

A circuit is derived from a list which contains all its elements.

Conceptually, every time an element is to be inserted in the circuit,
two operations have to be performed:

* The element must be appended to the ``Circuit`` instance.
* Its connections should be ensure checking that
  the nodes the element refers to are indeed existing circuit nodes.

To simplify the operation of adding a component to a ``Circuit``,
the following convenience methods are provided to the user to add and
remove most elements to the circuit:

* :func:`Circuit.add_resistor`
* :func:`Circuit.add_capacitor`
* :func:`Circuit.add_inductor`
* :func:`Circuit.add_vsource`
* :func:`Circuit.add_isource`
* :func:`Circuit.add_diode`
* :func:`Circuit.add_mos`
* :func:`Circuit.add_cccs`
* :func:`Circuit.add_vcvs`
* :func:`Circuit.add_vccs`
* :func:`Circuit.add_user_defined`
* :func:`Circuit.remove_elem`

Example:

.. code-block:: python

    mycircuit = circuit.Circuit(title="Example circuit", filename=None)
    # no filename since there will be no deck associated with this circuit.
    # get the ref node (gnd)
    gnd = mycircuit.get_ground_node()
    # add a node named n1 and a 600 ohm resistor connected between n1 and gnd
    mycircuit.add_resistor(part_id="R1", n1="n1", n2=gnd, R=600)

Refer to the methods help for additional information.

Nodes
\"\"\"\"\"

The nodes are internally stored in the following way: we assign to each node an
internal ID, independetly from its external identifier used in the netlist.
Those IDs are integers.

The simulator uses always the internal names. When the results are
presented to the user, the internal node is not showed, the external
identifier (or external node name) is printed instead.

This is done through:

.. code-block:: python

    my_circuit = Circuit()
    ...
    [ init code ]
    ...
    print "This is a node" + my_circuit.nodes_dict[int_node]

.. rubric:: Internal only nodes

The number of internal only nodes (added automatically by the simulator)
is held in ``Circuit.internal_nodes``. That value shouldn't be changed by
hand.

Device models
\"\"\"\"\"\"\"\"\"\"\"\"\"

Non-linear elements have their operation described by specialized routines
held in their module.

They are stored in ``Circuit.models`` (of type dict), the following methods
are provided to add and remove device models to a Circuit instance.

* :func:`Circuit.add_model`
* :func:`Circuit.remove_model`

Reference
\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\""\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"

"""

from __future__ import (unicode_literals, absolute_import,
                        division, print_function)

import copy
import math

from . import components
from . import diode
from . import ekv
from . import mosq
from . import printing
from . import py3compat
from . import switch

# will be added here by netlist_parser and circuit instances
user_defined_modules_dict = {}

[docs]class Circuit(list): """The circuit class. **Parameters:** title : string The circuit title. filename : string, optional .. deprecated:: 0.09 If the circuit instance corresponds to a netlist file on disk, set this to the netlist filename. """ def __init__(self, title, filename=None): self.title = title self.filename = filename self.nodes_dict = {} # {int_node:ext_node, int_node:ext_node} self.internal_nodes = 0 self.models = {} self.gnd = '0' def __str__(self): s = "* " + self.title + "\n" for elem in self: s += elem.get_netlist_elem_line(self.nodes_dict) + "\n" return s[:-1]
[docs] def create_node(self, name): """Creates a new circuit node If there is a node in the circuit with the same name, ValueError is raised. **Parameters:** name : string the _unique_ identifier of the node. **Returns:** node : string the _unique_ identifier of the node, to be used for subsequent element declarations, for example. :raises ValueError: if a new node with the given id cannot be created, for example because a node with the same name already exists in the circuit. The only exception is the ground node, which has the reserved id ``'0'``, and for which this method won't raise any exception. :raises TypeError: if the parameter ``name`` is not of "text" type (what that means exactly depends on which version of Python you are using.) """ if type(name) not in py3compat.string_types: raise TypeError("The node %s should have been of text type" % name) got_ref = 0 in self.nodes_dict if name not in self.nodes_dict: if name == '0': int_node = 0 else: int_node = int(len(self.nodes_dict)/2) + 1*(not got_ref) self.nodes_dict.update({int_node:name}) self.nodes_dict.update({name:int_node}) else: raise ValueError('Impossible to create new node %s: node exists!' % name) return name
[docs] def add_node(self, ext_name): """Adds the supplied node to the circuit, if needed. When a 'normal' (not the reference) node is added, a internal name (or label) is assigned to it. The nodes labels are stored in ``Circuit.nodes_dict``, as a dictionary of pairs like ``{int_node:ext_node}``. Those internal names are integers, by definition, and are generated starting from 1, then 2, 3, 4, 5... The integer ``0`` is reserved for the reference node (gnd), which is required for the circuit to be non-pathological and has ``ext_name=str(int_name)='0'``. Notice that this method doesn't halt or print errors if the node is already been added previsiously. It simply returns the internal node name assigned to it. **Parameters:** ext_name : string The unique identifier of the node. **Returns:** int_name : string the *unique* *internal* ciecuit identifier of the node. :raises TypeError: if the parameter ``ext_name`` is not of "text" type (what that means exactly depends on which version of Python you are using.) """ # must be text (str unicode...) if type(ext_name) not in py3compat.string_types: raise TypeError("The node %s should have been of text type" % ext_name) # test: do we already have it in the dictionary? if ext_name not in self.nodes_dict: if ext_name == '0': int_node = 0 else: got_ref = 0 in self.nodes_dict int_node = int(len(self.nodes_dict)/2) + 1*(not got_ref) self.nodes_dict.update({int_node:ext_name}) self.nodes_dict.update({ext_name:int_node}) else: int_node = self.nodes_dict[ext_name] return int_node
[docs] def new_internal_node(self): """Generate implicit internal nodes. Some devices are made of a group of other devices, connected by "internal only" nodes, which have the prefix ``'INT'`` and the simulator treats specially, hiding them from the user if not explicitly asked about them. This method generates the external names for such nodes and inserts them in the circuit. **Returns:** ext_node : string The corresponding external node id. """ ext_node = "INT" + str(self.internal_nodes) self.internal_nodes = self.internal_nodes + 1 self.create_node(ext_node) return ext_node
[docs] def get_nodes_number(self): """Returns the number of nodes in the circuit""" return int(len(self.nodes_dict)/2)
[docs] def is_int_node_internal_only(self, int_node): """Check whether an internal node is an "internal only node" or not. **Parameters:** int_node : int The internal only node to be checked. **Returns:** chk : boolean The result of the check. :raises TypeError: if the supplied node is not an ``int``. Typically this happens when the method is called with an *external* name. """ if type(int_node) is not int: raise TypeError('Expecting an INTERNAL node of type int, got %s.' % type(int_node)) return self.nodes_dict[int_node].find("INT") > -1
[docs] def is_nonlinear(self): """Check whether the circuit is non-linear or not. **Returns:** chk : boolean The result of the check. """ for elem in self: if elem.is_nonlinear: return True return False
[docs] def get_locked_nodes(self): """Get all nodes connected to non-linear elements. This list is meant to be passed to ``dc_solve`` or ``mdn_solver`` to be used in ``get_td`` to evaluate the damping coefficient in a Newton-Rhapson iteration. **Returns:** locked_nodes : list A list of internal nodes. """ locked_nodes = [] nl_elements = [elem for elem in self if elem.is_nonlinear] for elem in nl_elements: oports = elem.get_output_ports() for index in range(len(oports)): ports = elem.get_drive_ports(index) for port in ports: locked_nodes.append(port) return locked_nodes
[docs] def ext_node_to_int(self, ext_node): """This function returns the integer id associated with an external node id. **Parameters:** ext_node : string The external node id to be converted. **Returns:** int_node : int The internal node associated. """ return self.nodes_dict[ext_node]
[docs] def int_node_to_ext(self, int_node): """This function returns the string id associated with the integer internal node id ``int_node``. **Parameters:** int_node : int The internal node id to be converted. **Returns:** ext_node : string the string id associated with ``int_node``. """ return self.nodes_dict[int_node]
[docs] def has_duplicate_elem(self): """Self-check for duplicate elements. No circuit should ever have duplicate elements (ie elements with the same ``part_id``). **Returns:** chk : boolean The result of the check. """ all_ids = tuple(map(lambda e: e.part_id, self)) return len(set(all_ids)) != len(all_ids)
[docs] def get_ground_node(self): """Returns the reference node, AKA GND.""" return '0'
[docs] def get_elem_by_name(self, part_id): """Get a circuit element from its ``part_id`` value. If no matching element is found, the method returns ``None``. This may change in the future. **Parameters:** part_id : string The ``part_id`` of the element **Returns:** elem : circuit element Depending whether a matching element was found or not. :raises ValueError: if the element is not found. """ for e in self: if e.part_id.lower() == part_id.lower(): return e raise ValueError('Element %s not found' % part_id)
[docs] def add_model(self, model_type, model_label, model_parameters): """Add a model to the available circuit models. **Parameters:** model_type : string the model type (eg "ekv"). Right now, the possible values are: ``"mosq"``, ``"ekv"``, ``"diode"``, ``"sw"``. model_label : string a unique identifier for the model being added (eg. ``"nch1"``). model_parameters: dict a dictionary holding the parameters to be supplied to the model to instantiate it. """ if 'name' not in model_parameters: model_parameters.update({'name':model_label}) if model_type == "ekv": model_iter = ekv.ekv_mos_model(**model_parameters) model_iter.name = model_label elif model_type == "mosq": model_iter = mosq.mosq_mos_model(**model_parameters) model_iter.name = model_label elif model_type == "diode": model_iter = diode.diode_model(**model_parameters) model_iter.name = model_label elif model_type == "sw": model_iter = switch.vswitch_model(**model_parameters) model_iter.name = model_label else: raise CircuitError("Unknown model type %s" % (model_type,)) self.models.update({model_label: model_iter})
[docs] def remove_model(self, model_label): """Remove a model from the available models. **Parameters:** model_label : string the unique identifier corresponding to the model being removed. .. note:: This method currently silently ignores models that are not defined. """ if self.models is not None and model_label in self.models: del self.models[model_label]
# should print a warning here
[docs] def add_resistor(self, part_id, n1, n2, value): """Adds a resistor to the circuit. The resistor instance is added to the circuit elements and connected to the provided nodes. If the nodes are not found in the circuit, they are created and added as well. **Parameters:** part_id : string the resistor part_id (eg "R1"). The first letter is replaced by an R n1, n2 : string the nodes to which the resistor is connected. value : float, The resistance between ``n1`` and ``n2`` in Ohm. .. seealso:: :func:`add_resistor`, :func:`add_capacitor`, :func:`add_inductor`, :func:`add_vsource`, :func:`add_isource`, :func:`add_diode`, :func:`add_mos`, :func:`add_vcvs`, :func:`add_vccs`, :func:`add_cccs`, :func:`add_user_defined`, :func:`remove_elem` """ n1 = self.add_node(n1) n2 = self.add_node(n2) if value == 0: raise CircuitError("ZERO-valued resistors are not allowed.") elem = components.Resistor(part_id=part_id, n1=n1, n2=n2, value=value) self.append(elem)
[docs] def add_capacitor(self, part_id, n1, n2, value, ic=None): """Adds a capacitor to the circuit. The capacitor instance is added to the circuit elements and connected to the provided nodes. If the nodes are not found in the circuit, they are created and added as well. **Parameters:** part_id : string The capacitor part_id (eg "C1"). The first letter is always C. n1, n2 : string The nodes to which the element is connected. value : float The capacitance value. ic : float, optional The initial condition, if any. See the simulation docs for how this affects the results. .. seealso:: :func:`add_resistor`, :func:`add_inductor`, :func:`add_vsource`, :func:`add_isource`, :func:`add_diode`, :func:`add_mos`, :func:`add_vcvs`, :func:`add_vccs`, :func:`add_cccs`, :func:`add_user_defined`, :func:`remove_elem` """ if value == 0: raise CircuitError("ZERO-valued capacitors are not allowed.") n1 = self.add_node(n1) n2 = self.add_node(n2) elem = components.Capacitor(part_id=part_id, n1=n1, n2=n2, value=value, ic=ic) self.append(elem)
[docs] def add_inductor(self, part_id, n1, n2, value, ic=None): """Adds an inductor to the circuit. The inductor instance is added to the circuit elements and connected to the provided nodes. If the nodes are not found in the circuit, they are created and added as well. **Parameters:** part_id : string The inductor part_id (eg "Lfilter"). The first letter is always L. n1, n2 : string The nodes to which the element is connected. Eg. ``"in"`` or ``"out_a"``. value : float The inductance value. ic : float, optional Initial condition, see simulation types for how this affects the results. .. seealso:: :func:`add_resistor`, :func:`add_capacitor`, :func:`add_inductor`, :func:`add_vsource`, :func:`add_isource`, :func:`add_diode`, :func:`add_mos`, :func:`add_vcvs`, :func:`add_vccs`, :func:`add_cccs`, :func:`add_user_defined`, :func:`remove_elem` """ n1 = self.add_node(n1) n2 = self.add_node(n2) elem = components.Inductor(part_id=part_id, n1=n1, n2=n2, value=value, ic=ic) self.append(elem)
[docs] def add_inductor_coupling(self, part_id, L1, L2, value): """Add a coupling between two inductors. **Parameters:** part_id : string The part ID for the inductor coupling device. Eg. ``'K1'``, the first letter is always ``'K'``. L1 : string The part ID of the first inductor to be coupled. L2 : string The part ID of the second inductor to be coupled. value : float The ``k`` value of the mutual coupling coefficient. Its value must be greater than zero and lesser or equal to``1`` or instability ensues. """ L1elem, L2elem = None, None for e in self: if isinstance(e, components.Inductor) and (L1 == e.part_id): L1elem = e elif isinstance(e, components.Inductor) and (L2 == e.part_id): L2elem = e if L1elem is None or L2elem is None: error_msg = "One or more coupled inductors for %s were not found: %s (found: %s), %s (found: %s)." % \ (part_id, L1, L1elem is not None, L2, L2elem is not None) raise ValueError(error_msg) M = math.sqrt(L1elem.value * L2elem.value) * value elem = components.InductorCoupling(part_id=part_id, L1=L1, L2=L2, K=value, M=M) L1elem.coupling_devices.append(elem) L2elem.coupling_devices.append(elem) self.append(elem)
[docs] def add_vsource(self, part_id, n1, n2, dc_value, ac_value=0, function=None): """Adds a voltage source to the circuit (also takes care that the nodes are added as well). **Parameters:** part_id : string The voltage source part_id (eg "VA"). The first letter is always V. n1, n2 : string The nodes to which the element is connected. Eg. ``"in"`` or ``"out_a"``. dc_value : float DC voltage value ac_value : float, optional AC voltage value, defaults to 0. function : function, optional Time function. See devices.py for built-in options. """ n1 = self.add_node(n1) n2 = self.add_node(n2) elem = components.sources.VSource(part_id=part_id, n1=n1, n2=n2, dc_value=dc_value, ac_value=ac_value) if function is not None: elem.is_timedependent = True elem._time_function = function self.append(elem)
[docs] def add_isource(self, part_id, n1, n2, dc_value, ac_value=0, function=None): """Adds a current source to the circuit (also takes care that the nodes are added as well). **Parameters:** part_id : string The current source ID (eg ``"IA"`` or ``"I3"``). The first letter is always I. n1, n2 : string The nodes to which the element is connected, eg. ``"in"`` or ``"out1"``. dc_value : float DC current value. ac_value :float, optional AC current value, defaults to 0. function : function, optional Time function. See devices.py for built-in options. """ n1 = self.add_node(n1) n2 = self.add_node(n2) elem = components.sources.ISource(part_id=part_id, n1=n1, n2=n2, dc_value=dc_value, ac_value=ac_value) if function is not None: elem.is_timedependent = True elem._time_function = function self.append(elem)
[docs] def add_diode(self, part_id, n1, n2, model_label, models=None, Area=None, T=None, ic=None, off=False): """Adds a diode to the circuit (also takes care that the nodes are added as well). **Parameters:** part_id : string The diode ID (eg "D1"). The first letter is always D. n1, n2 : string the nodes to which the element is connected. eg. ``"in"`` or ``"out_a"`` model_label : string The diode model identifier. The model needs to be added first, then the elements using it. models : dict, optional List of available model instances. If not set or ``None``, the circuit models will be used (recommended). Area : float, optional Scaled device area (optional, defaults to 1.0) T : float, optional Operating temperature (no temperature dependence yet) ic : float, optional Initial condition (not really implemented yet) off : bool, optional Consider the diode to be initially off. """ n1 = self.add_node(n1) n2 = self.add_node(n2) if models is None: models = self.models if model_label not in models: raise ModelError("Unknown diode model id: " + model_label) elem = diode.diode(part_id=part_id, n1=n1, n2=n2, model=models[ model_label], AREA=Area, T=T, ic=ic, off=off) self.append(elem)
[docs] def add_mos(self, part_id, nd, ng, ns, nb, w, l, model_label, models=None, m=1, n=1): """Adds a mosfet to the circuit (also takes care that the nodes are added as well). **Parameters:** part_id : string The mos part_id (eg "M1"). The first letter is always M. nd : string The drain node. ng : string The gate node. ns : string The source node. nb : string The bulk node. w : float The gate width. l : float The gate length. model_label : string The model identifier. models : dict, optional The circuit models. m : int, optional Shunt multiplier value. Defaults to 1. n : int, optional Series multiplier value, not always supported. Defaults to 1. """ nd = self.add_node(nd) ng = self.add_node(ng) ns = self.add_node(ns) nb = self.add_node(nb) if models is None: models = self.models if model_label not in models: raise ModelError("Unknown model id: " + model_label) if isinstance(models[model_label], ekv.ekv_mos_model): elem = ekv.ekv_device(part_id, nd, ng, ns, nb, w, l, models[model_label], m, n) elif isinstance(models[model_label], mosq.mosq_mos_model): elem = mosq.mosq_device(part_id, nd, ng, ns, nb, w, l, models[model_label], m, n) else: raise Exception("Unknown model type for " + model_label) self.append(elem)
[docs] def add_cccs(self, part_id, n1, n2, source_id, value): """Adds a current-controlled current source (CCCS) to the circuit This method takes care that its nodes are added as well. **Parameters:** part_id : string The cccs ID (eg ``'F1'``). The first letter is always ``'F'``. n1, n2 : strings The output port nodes, where the output current is forced. Eg. "outp", "outm" or "out_a", "out_b". source_id : string The voltage source to be used to sense the current that drives the output. Eg. ``'V1'``. value : float The proportionality factor between input (:math:`I_s`) and output (:math:`I_o`) currents. Mathematically: .. math:: I_o = \\alpha I_s .. seealso:: :class:`ahkab.components.sources.FISource` """ # Add the nodes, this is SAFE: if a node is already known to the circuit, # the methods will just ignore the request. n1 = self.add_node(n1) n2 = self.add_node(n2) # instantiate the element elem = components.sources.FISource(part_id=part_id, n1=n1, n2=n2, source_id=source_id, value=value) # add it! self.append(elem)
[docs] def add_ccvs(self, part_id, n1, n2, source_id, value): """Adds a current-controlled voltage source (CCCS) to the circuit This method takes care that its nodes are added as well. **Parameters:** part_id : string The cccs ID (eg ``'H1'``). The first letter is always ``'H'``. n1, n2 : strings The output port nodes, where the output current is forced. Eg. "outp", "outm" or "out_a", "out_b". source_id : string The voltage source to be used to sense the current that drives the output voltage. Eg. ``'V1'``. value : float The proportionality factor between the sense current :math:`I_s` flowing into the ``source_id`` voltage source (input) and output voltage. Mathematically: .. math:: Vn_1 - Vn_2 = \\alpha I_s .. seealso:: :class:`ahkab.components.sources.EVSource`, :class:`ahkab.components.sources.FISource` """ # Add the nodes, this is SAFE: if a node is already known to the circuit, # the methods will just ignore the request. n1 = self.add_node(n1) n2 = self.add_node(n2) # instantiate the element elem = components.sources.HVSource(part_id=part_id, n1=n1, n2=n2, source_id=source_id, value=value) # add it! self.append(elem)
[docs] def add_vcvs(self, part_id, n1, n2, sn1, sn2, value): """Adds a voltage-controlled voltage source (vcvs) to the circuit This method also takes care that its nodes are added as well. **Parameters:** part_id : string The vcvs ID (eg "E1"). The first letter is always E. n1, n2 : string The output port nodes, where the output voltage is forced. Eg. "outp", "outm" or "out_a", "out_b". sn1, sn2 : string The input port nodes, where the input voltage is read. Eg. "inp", "inm" or "in_a", "in_b". alpha : float The proportionality factor between input and output voltages is given by the relationship: .. math:: V(out_p) - V(out_n) = \\alpha \\cdot (V(in_p) - V(in_n)) """ n1 = self.add_node(n1) n2 = self.add_node(n2) sn1 = self.add_node(sn1) sn2 = self.add_node(sn2) elem = components.sources.EVSource(part_id=part_id, n1=n1, n2=n2, sn1=sn1, sn2=sn2, value=value) self.append(elem)
[docs] def add_vccs(self, part_id, n1, n2, sn1, sn2, value): """Adds a voltage-controlled current source (VCCS) to the circuit This method also takes care that its nodes are added as well. **Parameters:** part_id : string The VCCS ID (eg ``"G1"``). The first letter is always ``'G'``. n1, n2 : string The output port nodes, where the output current is forced. Eg. "outp", "outm" or "out_a", "out_b". The passive convention is used as everywhere else in the simulator: a positive current flows into ``n1`` and out of ``n2``. sn1, sn2 : string The input port nodes, where the input voltage is sensed. Eg. "inp", "inm" or "in_a", "in_b". value : float The proportionality factor between input and output voltages, which are related by the equality: .. math:: I_o = alpha * \\left[V(inp) - V(inn)\\right] """ n1 = self.add_node(n1) n2 = self.add_node(n2) sn1 = self.add_node(sn1) sn2 = self.add_node(sn2) elem = components.sources.GISource(part_id=part_id, n1=n1, n2=n2, sn1=sn1, sn2=sn2, value=value) self.append(elem)
[docs] def add_switch(self, part_id, n1, n2, sn1, sn2, ic, model_label, models=None): """Adds a voltage-controlled or current-controlled switch to the circuit This method also takes care that its nodes are added to the circuit as well, if necessary. **Notice:** - Current-controlled switches are not yet implemented. If you try to add one, you'll trigger an error. If you got a bit of time to spare, patches are welcome. - The switches ``part_id`` should begin with ``'S'`` for voltage-controlled switches and with ``'W'`` for current-controlled switches. - The actual behavior is set by the model. Make sure you supply a voltage-controlled switch model for a voltage-controlled switch and the appropriate type of model for the current-controlled switch. Mixing them up will go undetected. **Parameters:** part_id : string the switch ID (eg ``"S1"`` - voltage-controlled - or ``"Wa"`` - current-controlled). The first letter is always ``S`` or ``W``. n1, n2 : string the output port nodes, where the switch is connected. Eg. ``"out1"``, ``"out2"`` or ``"n_a"``, ``"n_b"``. sn1, sn2 : string The input port nodes, where the input voltage is read. Eg. "inp", "inm" or "in_a", "in_b". ic : boolean The initial conditions for transient simulation. Not currently implemented! model_label : string The switch model identifier. The model needs to be added first, then the elements using it. models : dict, optional A dictionary assembled as (identifier:instance), containing all the available model instances. If not set or ``None``, the circuit models will be used (recommended). """ n1 = self.add_node(n1) n2 = self.add_node(n2) sn1 = self.add_node(sn1) sn2 = self.add_node(sn2) if models is None: models = self.models if model_label not in models: raise ModelError("Unknown switch model id: " + model_label) elem = switch.switch_device(part_id=part_id, n1=n1, n2=n2, sn1=sn1, sn2=sn2, model=models[model_label]) self.append(elem)
[docs] def add_user_defined(self, module, label, param_dict): """Adds a user defined element. In order for this to work, you should write a module that supplies the elem class. XXX WRITE DOC """ if module_name in circuit.user_defined_modules_dict: module = circuit.user_defined_modules_dict[module_name] else: fp, pathname, description = imp.find_module(module_name) module = imp.load_module(module_name, fp, pathname, description) circuit.user_defined_modules_dict.update({module_name: module}) elem_class = getattr(module, label) param_dict.update({"convert_units": convert_units}) param_dict.update({"circuit_node": self.add_node}) elem = elem_class(**param_dict) elem.part_id = "y%s" % part_id[1:] # call check() if supported if hasattr(elem, "check"): selfcheck_result, error_msg = elem.check() if not selfcheck_result: raise NetlistParseError("module: " + module_name + \ " elem type: " + elem_type_name + \ " error: " + error_msg) self.append(elem)
[docs] def remove_elem(self, elem_or_id): """Removes an element from the circuit and takes care that no "orphan" nodes are left. .. note:: Support for removing elements is experimental. **Parameters:** elem_or_id : string or circuit element You may pass as first element, alternatively, either the ``part_id`` of the element to be removed or the element itself. The method will also take care of purging from the circuit nodes that are left orphan, ie with no elements connected. :raises ValueError: if no such element is found in the circuit. """ if type(elem_or_id) in py3compat.string_types: #we got a part_id, we need the element elem = self.get_elem_by_name(elem_or_id) else: # we got the element elem = elem_or_id self.remove(elem) nodes = [] if hasattr(elem, 'n1') and elem.n1 != 0: nodes = nodes + [elem.n1] if hasattr(elem, 'n2') and elem.n2 != 0 and elem.n2 not in nodes: nodes = nodes + [elem.n2] if elem.is_nonlinear: for n1, n2 in elem.ports: if n1 != 0 and n1 not in nodes: nodes = nodes + [n1] if n2 != 0 and n2 not in nodes: nodes = nodes + [n2] # check if then nodes are needed by other elements remove_list = copy.copy(nodes) for n in nodes: for e in self: if hasattr(elem, 'n1') and e.n1 == n or \ hasattr(elem, 'n2') and e.n2 == n or \ hasattr(elem, 'sn1') and e.sn1 == n or \ hasattr(elem, 'sn2') and e.sn2 == n: remove_list.remove(n) break if elem.is_nonlinear: oports = elem.get_output_ports() # check output ports for n1, n2 in oports: if n1 == n or n2 == n: remove_list.remove(n) break if n not in remove_list: break # check the ports that drive them for i in range(len(oports)): dports = elem.get_drive_ports(i) for n1, n2 in oports: if n1 == n or n2 == n: remove_list.remove(n) break if n not in remove_list: break for n in remove_list: self.nodes_dict.pop(self.nodes_dict[n]) self.nodes_dict.pop(n)
[docs] def find_vde_index(self, elem_or_id, verbose=3): """Finds a voltage-defined element MNA index. **Parameters:** elem_or_id : string or circuit element You may pass as first element, alternatively, either the ``part_id`` of the element whose index is being requested (eg. 'V1') or the element itself. Notice the ``part_id`` includes both the id letter (eg. 'V') and the description (eg. '1'). verbose : int The verbosity level, from 0 (silent) to 6 (debug). **Returns:** indx : int The index. :raises ValueError: if no such element is in the circuit. """ if type(elem_or_id) not in py3compat.string_types: # we got an element part_id = elem_or_id.part_id else: # we got a string corresponding to the part_id of an element part_id = elem_or_id vde_index = 0 for elem in self: if is_elem_voltage_defined(elem): if elem.part_id.upper() == part_id.upper(): break else: vde_index += 1 else: raise ValueError(("find_vde_index(): element %s was not found." +\ " This is a bug.") % (part_id,)) printing.print_info_line(("%s found at index %d" % (part_id, vde_index), 6), verbose) return vde_index
[docs] def find_vde(self, index): """Finds a voltage-defined element from its MNA KVL index **Parameters:** index : int The element index in the KVL equations. **Returns:** e : circuit element (an instance of a subclass of Component) The element corresponding to ``index``. :raises IndexError: if no element corresponds to such an index. """ index = index - len(self.nodes_dict)/2 + 1 ni = 0 for e in self: if is_elem_voltage_defined(e): if index == ni: break else: ni = ni + 1 else: #executed if no break occurred raise IndexError('No element corresponds to vde index %d' % (index + len(self.nodes_dict)/2 - 1)) return e
# STATIC METHODS
[docs]def is_elem_voltage_defined(elem): """Check if an element needs its own KCL equation **Parameters:** elem : Component The element to be checked. **Returns:** chk : bool ``True`` if ``elem`` is a voltage source, an inductor, a voltage-controlled voltage source or a current-controlled voltage source. ``False`` otherwise. """ if isinstance(elem, components.sources.VSource) or isinstance(elem, components.sources.EVSource) or \ isinstance(elem, components.sources.HVSource) or isinstance(elem, components.Inductor) \ or (hasattr(elem, "is_voltage_defined") and elem.is_voltage_defined): return True else: return False
[docs]class NodeNotFoundError(Exception): """Circuit Node exception.""" pass
[docs]class CircuitError(Exception): """General circuit assembly exception.""" pass
[docs]class ModelError(Exception): """Model not found exception.""" pass
[docs]class subckt: """This class holds the necessary information about a circuit. An instance of this class is returned by: :func:`ahkab.netlist_parser.parse_sub_declaration` **Parameters:** name : string The subcircuit definition label. code : string The netlist code that can be instantiated have a circuit instance. connected_nodes_list : list A list of nodes that are used in the circuit and that are meant to be connected to the external circuit. Notice that in the current implementation, the GND node (0) is *always* global. """ def __init__(self, name, code, connected_nodes_list): self.name = name self.connected_nodes_list = connected_nodes_list self.code = code
class _circuit_wrapper: """Fictious circuit class, meant to wrap subcircuits. Not meant for end users at this stage. Rationale: Within a subcircuit, the nodes name are fictious. All nodes have to be renamed before a subcircuit instance may be insterted in a circuit (all our circuits are flat in memory for now). The nodes of the subcircuit that are connected to the nodes of the circuit have to be renamed to them, those that are not referenced there need to be renamed in order for them ot be unique. This class wraps a circuit object and performs the conversion _before_ calling ``circ.add_node()``. While instatiating/calling a subcircuit wrap the circuit instance in this. **Parameters:** circ : circuit instance The main circuit. Remember that all our assembled circuits are flat in memory. connection_nodes_dict : dictionary The dictionary mapping internal nodes to global, circuit-wide nodes. subckt_name : string The subcircuit instance name. The first letter must always be ``'X'``. subckt_label : string The label of the subcircuit that is being instantiated. """ def __init__(self, circ, connection_nodes_dict, subckt_name, subckt_label): self.circ = circ self.prefix = subckt_label + "-" + subckt_name + "-" self.subckt_node_filter_dict = {} self.subckt_node_filter_dict.update(connection_nodes_dict) self.subckt_node_filter_dict.update({'0': '0'}) def add_node(self, ext_name): """We want to perform the following conversions: * connected node in the subcircuit -> node in the upper circuit * local-only node of the subcircuit -> rename it to something unique * REF (0) -> REF (0) And then call ``circ.add_node()``. """ if ext_name not in self.subckt_node_filter_dict: self.subckt_node_filter_dict.update( {ext_name: self.prefix + ext_name}) int_node = self.circ.add_node(self.subckt_node_filter_dict[ext_name]) return int_node