Source code for ahkab.symbolic

# -*- coding: iso-8859-1 -*-
# symbolic.py
# Symbolic simulation module
# Copyright 2010-2013 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/>.

"""
This module provides the functionality needed to perform a small-signal symbolic
simulation.

The principal method is :func:`symbolic_analysis`, which performs the symbolic
circuit solution.

.. note::

    This module is geared towards *setting up and running* the symbolic
    simulation.  Typically, it should be used in conjunction with
    :class:`ahkab.results.symbolic_solution`, the symbolic solution class, which
    holds several convenience methods to extensively manipulate, simplify,
    post-process and analyze the simulation results, complementing the
    functionality offered by the Sympy module itself.

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

"""

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

import sympy
from sympy.matrices import zeros as smzeros

from . import circuit
from . import components
from . import ekv
from . import mosq
from . import diode
from . import printing
from . import py3compat
from . import results
from . import options

specs = {'symbolic': {'tokens': ({
                                  'label': 'tf',
                                  'pos': None,
                                  'type': str,
                                  'needed': False,
                                  'dest': 'source',
                                  'default': None
                                 },
                                 {
                                  'label': 'ac',
                                  'pos': None,
                                  'type': bool,
                                  'needed': False,
                                  'dest': 'ac_enable',
                                  'default': True
                                 },
                                 {
                                  'label': 'r0s',
                                  'pos': None,
                                  'type': bool,
                                  'needed': False,
                                  'dest': 'r0s',
                                  'default': False
                                 }
                                )
                     }
        }

#: the Laplace variable
s = sympy.Symbol('s', complex=True)

enabled_assumptions = {'real':True, 'positive':True, 'complex':True}

# When the MNA analysis is formulated in terms of conductances, we need
# to:
# * create a conductance symbol corresponding to the resistor symbol give by the
#   user,
# * be able to avoid collisions (distiguish) between these artificial
#   conductances and a user-defined in-circuit trans-conductance (ie a
#   VCCS or ISource), as their part_id is ``G<alphanum>``.
#
# To do so, we create symbols that are named:
# ``'G'`` + _COND_POSTFIX + part_id[1:]
#
_COND_POSTFIX = 'xxx'

[docs]def symbolic_analysis(circ, source=None, ac_enable=True, r0s=False, subs=None, outfile=None, verbose=3): """Attempt a symbolic, small-signal solution of the circuit. **Parameters:** circ : circuit instance the circuit instance to be simulated. source : string, optional the ``part_id`` of the source to be used as input for the transfer function. If ``None``, no transfer function is evaluated. ac_enable : bool, optional take frequency dependency into consideration (default: True). r0s : bool, optional take transistors' output impedance into consideration (default: False) subs: dict, optional a dictionary of part IDs to be substituted. It makes solving the circuit easier. Eg. ``subs={'R1':'R2'}`` - replace the resistor R1 with R2. outfile : string, optional output filename - ``'stdout'`` means print to stdout, the default. verbose: int, optional verbosity level 0 (silent) to 6 (painful). **Returns:** sol : symbolic solution The solutions. tfs : symbolic solution The transfer functions, only if requested. Otherwise ``tfs`` is a ``None`` object. """ if subs is None: subs = {} # no subs by default else: subs = _parse_substitutions(subs) if not ac_enable: printing.print_info_line( ("Starting symbolic small-signal DC analysis...", 1), verbose) else: printing.print_info_line( ("Starting symbolic AC analysis...", 1), verbose) printing.print_info_line( ("Building symbolic MNA, N and x...", 3), verbose, print_nl=False) mna, N, subs_g = generate_mna_and_N(circ, opts={'r0s': r0s}, ac=ac_enable, subs=subs, verbose=verbose) x = get_variables(circ) mna = mna[1:, 1:] N = N[1:, :] printing.print_info_line((" done.", 3), verbose) printing.print_info_line(("MNA matrix (reduced):", 5), verbose) printing.print_info_line((sympy.sstr(mna), 5), verbose) printing.print_info_line(("N matrix (reduced):", 5), verbose) printing.print_info_line((sympy.sstr(N), 5), verbose) printing.print_info_line(("Building equations...", 3), verbose) eq = [] for i in _to_real_list(mna * x + N): eq.append(sympy.Eq(i, 0)) x = _to_real_list(x) if verbose > 3: printing.print_symbolic_equations(eq) printing.print_info_line(("To be solved for:", 4), verbose) printing.print_info_line((str(x), 4), verbose) printing.print_info_line(("Solving...", 1), verbose) sol = sympy.solve( eq, x, manual=options.symb_sympy_manual_solver, simplify=True) for ks in list(sol.keys()): sol.update({ks: sol[ks].subs(subs_g)}) if sol == {}: printing.print_warning("No solutions. Check the netlist.") else: printing.print_info_line(("Success!", 2), verbose) printing.print_info_line(("Results:", 1), verbose) if options.cli: printing.print_symbolic_results(sol) if source is not None: src = _symbol_factory(source.upper()) printing.print_info_line(("Calculating small-signal symbolic transfer functions (%s))..." % (str(src),), 2), verbose, print_nl=False) tfs = calculate_gains(sol, src) printing.print_info_line(("done.", 2), verbose) printing.print_info_line( ("Small-signal symbolic transfer functions:", 1), verbose) if options.cli: printing.print_symbolic_transfer_functions(tfs) else: tfs = None # convert to a results instance sol = results.symbolic_solution(sol, subs, circ, outfile) if tfs: if outfile and outfile != 'stdout': outfile += ".tfs" tfs = results.symbolic_solution(tfs, subs, circ, outfile, tf=True) return sol, tfs
[docs]def calculate_gains(sol, xin, optimize=True): """Calculate low-frequency gain and roots of a transfer function. **Parameters:** sol : dict the circuit solution xin : Sympy symbol the input variable optimize : boolean, optional If ``optimize`` is set to ``False``, no algebraic simplification will be attempted on the results. The default (``optimize=True``) results in ``sympy.together`` being called on each expression. **Returns:** gs : dict A dictionary with as keys the strings <key>/<xin> and as values dictionaries with keys ``'gain'``, ``'gain0'``, ``'poles'``, ``'zeros'``. """ gains = {} for key, value in sol.items(): tf = {} gain = sympy.together(value.diff(xin)) if optimize else value.diff(xin) (ps, zs) = get_roots(gain) tf.update({'gain': gain}) tf.update({'gain0': gain.subs(s, 0)}) tf.update({'poles': ps}) tf.update({'zeros': zs}) gains.update({"%s/%s" % (str(key), str(xin)): tf}) return gains
[docs]def get_variables(circ): """Get a sympy matrix containing the circuit variables to be solved for. **Parameters:** circ : circuit instance The circuit **Returns:** vars : sympy matrix, shape (n, 1) The variables in a column vector. """ # numero di soluzioni di tensione (al netto del ref) nv_1 = circ.get_nodes_number() - 1 # descrizioni dei componenti non definibili in tensione idescr = [elem.part_id.upper() for elem in circ if circuit.is_elem_voltage_defined(elem)] mna_size = nv_1 + len(idescr) x = smzeros(mna_size, 1) for i in range(mna_size): if i < nv_1: x[i, 0] = _symbol_factory("V" + str(circ.nodes_dict[i + 1])) else: x[i, 0] = _symbol_factory("I[" + idescr[i - nv_1] + "]") return x
def _to_real_list(M): """ M.tolist() returns a list of lists, even when the symb matrix is really just a vector. we want a list of symbols! This fixes that. mylist[k] = mymat.tolist[k][0] M: a sympy matrix with only one column Returns: a list. """ fakelist = M.tolist() reallist = [] for elem in fakelist: reallist.append(elem[0]) return reallist
[docs]def generate_mna_and_N(circ, opts, ac=False, subs=None, verbose=3): """Generate a symbolic Modified Nodal Analysis matrix and N vector. Only elements that have an ``is_symbolic`` attribute set to ``True`` (the default) are considered symbolically. Simply set the attribute to ``False`` to employ the numeric value. This allows to simplify and speed up the work of the symbolic solver. The formulation can be performed using conductances or resistances. The choice is made setting the global ``options.symb_formulate_with_gs`` value to ``True``. A formulation done in terms of resistors, may result in many separate :math:`1/R` terms in the matrices. Historically, ``sympy`` choked on those, because of a long-standing bug in polys. Now the issue seems to have been solved and the two computations should be symbolically equivalent albeit computationally different (as expected). The option value ``options.symb_formulate_with_gs`` is provided to restore the old functionality in case you use an old version of ``sympy``. **Parameters:** circ : circuit instance The circuit. opts : dict The options to be used for the generation of the matrices. As of now, the only supported option is ``'r0s'`` which can be set to either ``True`` or ``False``, and selects whether the equivalent output resistance of the transistors should be taken into account or not. ac : bool, optional Flag to trigger the inclusion of frequency-dependent elements. Defaults to ``False`` currently (but may change). subs : dict, optional The substitution dictionary, composed by mappings of ``<symbol>``:``<sympy expression>``. verbose : int, optional Verbosity flag, from ``0`` (silent) to ``6`` (very logorrhoic). Defaults to ``3``. **Returns:** mna, N : Sympy matrices The MNA matrix and the contant term of symbolic type. subs_gs : dict of symbols In case the formulation of the MNA is performed in terms of conducatances, this dictionary is to be used to substitute away the conducatances for the resistor symbols, after the circuit is solved but before the results are shown to the user. ``sympy``'s ``sub()`` can take care of that for you. If not necessary, this dictionary is empty. .. note:: Setting ``opts['r0s'] = True``, ie considering all the transistors output resistances, can significantly slow down -- or even prevent by consuming all available memory -- the solution of complex circuits with several active elements. We recommend a combination of the following: * setting the above option in simple circuits only, * inserting explicitely the :math:`r_0` you wish to consider at circuit level, * beefing up your machine with extra RAM and extra computing power, * being patient. """ n_of_nodes = circ.get_nodes_number() mna = smzeros(n_of_nodes) N = smzeros(n_of_nodes, 1) subs_g = {} for elem in circ: if isinstance(elem, components.Resistor): # we use conductances instead of 1/R because there is a significant # overhead handling many 1/R terms in sympy. if elem.is_symbolic: R = _symbol_factory(elem.part_id.upper(), real=True, positive=True) if R in subs: # instant substitution R = subs[R] if options.symb_formulate_with_gs: G = _symbol_factory('Gxxx' + str(R)[1:], real=True, positive=True) # but we keep track of which is which and substitute back after # solving. subs_g.update({G:1/R}) else: G = 1.0/R else: G = 1.0/elem.value mna[elem.n1, elem.n1] = mna[elem.n1, elem.n1] + G mna[elem.n1, elem.n2] = mna[elem.n1, elem.n2] - G mna[elem.n2, elem.n1] = mna[elem.n2, elem.n1] - G mna[elem.n2, elem.n2] = mna[elem.n2, elem.n2] + G elif isinstance(elem, components.Capacitor): if ac: if elem.is_symbolic: capa = _symbol_factory( elem.part_id.upper(), real=True, positive=True) if capa in subs: # instant substitution capa = subs[capa] else: capa = elem.value mna[elem.n1, elem.n1] = mna[elem.n1, elem.n1] + s * capa mna[elem.n1, elem.n2] = mna[elem.n1, elem.n2] - s * capa mna[elem.n2, elem.n2] = mna[elem.n2, elem.n2] + s * capa mna[elem.n2, elem.n1] = mna[elem.n2, elem.n1] - s * capa else: pass elif isinstance(elem, components.Inductor): pass elif isinstance(elem, components.sources.GISource): if elem.is_symbolic: alpha = _symbol_factory(elem.part_id.upper(), real=True) if alpha in subs: # instant substitution alpha = subs[alpha] else: alpha = elem.value mna[elem.n1, elem.sn1] = mna[elem.n1, elem.sn1] + alpha mna[elem.n1, elem.sn2] = mna[elem.n1, elem.sn2] - alpha mna[elem.n2, elem.sn1] = mna[elem.n2, elem.sn1] - alpha mna[elem.n2, elem.sn2] = mna[elem.n2, elem.sn2] + alpha elif isinstance(elem, components.sources.ISource): if elem.is_symbolic: IDC = _symbol_factory(elem.part_id.upper()) if IDC in subs: # instant substitution IDC = subs[IDC] else: IDC = elem.dc_value N[elem.n1, 0] = N[elem.n1, 0] + IDC N[elem.n2, 0] = N[elem.n2, 0] - IDC elif isinstance(elem, mosq.mosq_device) or isinstance(elem, ekv.ekv_device): gm = _symbol_factory('gm_' + elem.part_id, real=True, positive=True) if gm in subs: # instant substitution gm = subs[gm] mna[elem.n1, elem.ng] = mna[elem.n1, elem.ng] + gm mna[elem.n1, elem.n2] = mna[elem.n1, elem.n2] - gm mna[elem.n2, elem.ng] = mna[elem.n2, elem.ng] - gm mna[elem.n2, elem.n2] = mna[elem.n2, elem.n2] + gm if opts['r0s']: r0 = _symbol_factory( 'r0_' + elem.part_id, real=True, positive=True) if r0 in subs: # instant substitution r0 = subs[r0] mna[elem.n1, elem.n1] = mna[elem.n1, elem.n1] + 1 / r0 mna[elem.n1, elem.n2] = mna[elem.n1, elem.n2] - 1 / r0 mna[elem.n2, elem.n1] = mna[elem.n2, elem.n1] - 1 / r0 mna[elem.n2, elem.n2] = mna[elem.n2, elem.n2] + 1 / r0 elif isinstance(elem, diode.diode): gd = _symbol_factory("g" + elem.part_id, positive=True) if gd in subs: # instant substitution gd = subs[gd] mna[elem.n1, elem.n1] = mna[elem.n1, elem.n1] + gd mna[elem.n1, elem.n2] = mna[elem.n1, elem.n2] - gd mna[elem.n2, elem.n1] = mna[elem.n2, elem.n1] - gd mna[elem.n2, elem.n2] = mna[elem.n2, elem.n2] + gd elif isinstance(elem, components.sources.FISource): # These are added after all VDEs have been accounted for pass elif isinstance(elem, components.InductorCoupling): pass # this is taken care of within the inductors elif circuit.is_elem_voltage_defined(elem): pass # we'll add its lines afterwards elif verbose: printing.print_warning("Skipped elem %s: not implemented." % (elem.part_id.upper(),)) pre_vde = mna.shape[0] for elem in circ: if circuit.is_elem_voltage_defined(elem): index = mna.shape[0] # get_matrix_size(mna)[0] mna = _expand_matrix(mna, add_a_row=True, add_a_col=True) N = _expand_matrix(N, add_a_row=True, add_a_col=False) # KCL mna[elem.n1, index] = +1 mna[elem.n2, index] = -1 # KVL mna[index, elem.n1] = +1 mna[index, elem.n2] = -1 if isinstance(elem, components.sources.VSource): if elem.is_symbolic: VDC = _symbol_factory(elem.part_id.upper()) if VDC in subs: # instant substitution VDC = subs[VDC] else: VDC = elem.dc_value N[index, 0] = -VDC elif isinstance(elem, components.sources.EVSource): if elem.is_symbolic: alpha = _symbol_factory(elem.part_id.upper(), real=True) if alpha in subs: # instant substitution alpha = subs[alpha] else: alpha = elem.alpha mna[index, elem.sn1] = -alpha mna[index, elem.sn2] = +alpha elif isinstance(elem, components.sources.HVSource): if elem.is_symbolic: alpha = _symbol_factory(elem.part_id.upper(), real=True) if alpha in subs: # instant substitution alpha = subs[alpha] else: alpha = elem.alpha source_index = circ.find_vde_index(elem.source_id) mna[index, n_of_nodes + source_index] = +alpha elif isinstance(elem, components.Inductor): if ac: if elem.is_symbolic: L = _symbol_factory( elem.part_id.upper(), real=True, positive=True) if L in subs: # instant substitution L = subs[L] else: L = elem.L mna[index, index] = -s * L else: pass # already so: commented out # N[index,0] = 0 else: raise circuit.CircuitError('Element %s is not supported. ' + 'Please report this bug.' % elem.__class__) for elem in circ: if ac and isinstance(elem, components.Inductor): # find its index to know which column corresponds to its # current this_index = circ.find_vde_index(elem.part_id, verbose=0) for cd in elem.coupling_devices: if cd.is_symbolic: M = _symbol_factory( cd.part_id, real=True, positive=True) if M in subs: # instant substitution M = subs[M] else: M = cd.K # get `part_id` of the other inductor (eg. "L32") other_id_wdescr = cd.get_other_inductor(elem.part_id) # find its index to know which column corresponds to # its current other_index = circ.find_vde_index( other_id_wdescr, verbose=0) # add the term. mna[pre_vde + this_index, pre_vde + other_index] += -s * M elif isinstance(elem, components.sources.FISource): source_current_index = circ.find_vde_index(elem.source_id, verbose=0) if elem.is_symbolic: F = _symbol_factory(elem.part_id, real=True) if F in subs: # instant substitution F = subs[F] else: F = elem.alpha mna[elem.n1, pre_vde + source_current_index] += +F mna[elem.n2, pre_vde + source_current_index] += -F else: pass # all done return mna, N, subs_g
def _expand_matrix(mat, add_a_row=False, add_a_col=False): if add_a_row: row = smzeros(1, mat.shape[1]) mat = mat.row_insert(mat.shape[0], row) if add_a_col: col = sympy.zeros(mat.shape[0], 1) mat = mat.col_insert(mat.shape[1], col) return mat
[docs]def get_roots(expr): """Given the transfer function ``expr``, returns ``poles, zeros``. """ num, den = sympy.fraction(expr) return sympy.solve(den, s), sympy.solve(num, s)
def _parse_substitutions(sdict): """Generates a symbolic substitution dictionary from a dictionary of substitutions expressed in terms of part IDs. This method is typically called by :func:`symbolic_analysis`. .. note:: For what regards the resistors, the method will check the value of ``options.symb_formulate_with_gs`` and convert the resistors to conductances as needed, according to the option value. **Parameters:** sdict : a dict of strings The elements of the dictionary should be according to the syntax ``{'<part_id1>:<part_id2>'}``, eg ``{'R2':'R1'}``, instructing the simulator to substitute ``R1`` in place of ``R2``. **Returns:** subs : dict the dictionary of symbols to be passed to :func:`symbolic_analysis`. """ subs = {} for v1, v2 in py3compat.iteritems(sdict): if v1[0].upper() == 'R' and options.symb_formulate_with_gs: letter_id1 = 'G' + _COND_POSTFIX letter_id2 = 'G' + _COND_POSTFIX else: letter_id1 = v1[0].upper() letter_id2 = v2[0].upper() if letter_id1[0] in ('R', 'G', 'L', 'C', 'M'): s1 = _symbol_factory(letter_id1 + v1[1:], real=True, positive=True) else: s1 = _symbol_factory(letter_id1 + v1[1:], real=True) if letter_id2[0] in ('R', 'G', 'L', 'C', 'M'): s2 = _symbol_factory(letter_id2 + v2[1:], real=True, positive=True) else: s2 = _symbol_factory(letter_id2 + v2[1:], real=True) subs.update({s1:s2}) return subs def _symbol_factory(name, **options): filtered_options = {} for i in options: if options[i] and enabled_assumptions[i]: filtered_options.update({i:options[i]}) else: pass # discarded return sympy.Symbol(name.upper(), **filtered_options)