# -*- coding: iso-8859-1 -*-
# dc_analysis.py
# DC simulation methods
# 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/>.
"""
This module provides the functions needed to perform OP and DC simulations.
The principal are:
* :func:`dc_analysis` - which performs a dc sweep,
* :func:`op_analysis` - which does an operation point analysis or
Notice that internally, :func:`dc_analysis` calls :func:`op_analysis`,
since a DC sweep is nothing but a series of OP analyses..
The actual circuit solution is done by :func:`mdn_solver`, that uses a
modified version of the Newton Rhapson method.
Module reference
################
"""
from __future__ import (unicode_literals, absolute_import,
division, print_function)
import sys
import re
import copy
import numpy as np
import numpy.linalg
import scipy.sparse
import scipy.sparse.linalg
from . import components
from . import diode
from . import constants
from . import ticker
from . import options
from . import circuit
from . import printing
from . import utilities
from . import dc_guess
from . import results
from .utilities import convergence_check
specs = {'op': {
'tokens': ({
'label': 'guess',
'pos': None,
'type': bool,
'needed': False,
'dest': 'guess',
'default': options.dc_use_guess
},
{
'label': 'ic_label',
'pos': None,
'type': str,
'needed': False,
'dest': 'x0',
'default': None
}
)
},
'dc': {'tokens': ({
'label': 'source',
'pos': 0,
'type': str,
'needed': True,
'dest': 'source',
'default': None
},
{
'label': 'start',
'pos': 1,
'type': float,
'needed': True,
'dest': 'start',
'default': None
},
{
'label': 'stop',
'pos': 2,
'type': float,
'needed': True,
'dest': 'stop',
'default': None
},
{
'label': 'step',
'pos': 3,
'type': float,
'needed': True,
'dest': 'step',
'default': None
},
{
'label': 'type',
'pos': None,
'type': str,
'needed': False,
'dest': 'sweep_type',
'default': options.dc_lin_step
}
)
}
}
[docs]def dc_solve(mna, Ndc, circ, Ntran=None, Gmin=None, x0=None, time=None,
MAXIT=None, locked_nodes=None, skip_Tt=False, verbose=3):
"""Low-level method to perform a DC solution of the circuit
.. note::
Typically the user calls :func:`dc_analysis.op_analysis` or
:func:`dc_analysis.dc_analysis`, which in turn will setup all
matrices and call this method on their behalf.
The system we want to solve is:
.. math::
(mna + G_{min}) \\cdot x + N(t) + T(x, t) = 0
Where:
* :math:`mna` is the reduced MNA matrix with the required KVL/KCL rows
* :math:`N` is composed by a DC part, :math:`N_{dc}`, and a dynamic
time-dependent part :math:`N_{tran}(t)` and a time-dependent part
:math:`T_t(t)`.
* :math:`T(x, t)` is both time-dependent and non-linear with respect to
the circuit solution :math:`x`, and it will be built at each iteration
over :math:`t` and :math:`x`.
**Parameters:**
mna : ndarray
The MNA matrix described above. It can be built calling
:func:`generate_mna_and_N`. This matrix will contain the dynamic
component due to a Differetiation Formula (DF) when this method is
called from a transient analysis.
Ndc : ndarray
The DC part of :math:`N`. Also this vector may be built calling
:func:`generate_mna_and_N`.
circ : Circuit instance
The circuit instance from which ``mna`` and ``N`` were built.
Ntran : ndarray, optional
The linear time-dependent and *dynamic* part of :math:`N`, if available.
Notice this is typically set when a DF being applied and the method is
being called from a transient analysis.
Gmin : ndarray, optional
A matrix of the same size of ``mna``, containing the minimum
transconductances to ground. It can be built with
:func:`build_gmin_matrix`. If not set, no Gmin matrix is used.
x0 : ndarray or results.op_solution instance, optional
The initial guess for the Newthon-Rhapson algorithm. If not specified,
the all-zeros vector will be used.
time : float scalar, optional
The time at which any matrix evaluation done by this method will be
performed. Do not set for DC or OP analysis, must be set for a
transisent analysis. Notice that :math:`t=0` is not the same as DC!
MAXIT : int, optional
The maximum number of Newton Rhapson iterations to be performed before
giving up. If unset, ``options.dc_max_nr_iter`` is used.
locked_nodes : list of tuples, optional
The nodes that need to have a well behaved, slowly varying voltage
applied. Typically they control non-linear elements. This is generated
by :func:`ahkab.circuit.Circuit.get_locked_nodes` and it will be
generated for you if left unset. However, if you are doing many
simulations of the same circuit (as it happens in a transient
analysis), it's a good idea to generate it only once.
skip_Tt : boolean, optional
Do not build the :math:`T_t(t)` vector. Defaults to ``False``.
verbose : int, optional
The verbosity level. From 0 (silent) to 6 (debug). Defaults to 3.
**Returns:**
x : ndarray
The solution, if found.
error : ndarray
The error associated with each solution item, if it was found.
converged : boolean
A flag set to True when convergence was detected.
tot_iterations : int
Total number of NR iterations run.
"""
if MAXIT == None:
MAXIT = options.dc_max_nr_iter
if locked_nodes is None:
locked_nodes = circ.get_locked_nodes()
mna_size = mna.shape[0]
nv = circ.get_nodes_number()
tot_iterations = 0
if Gmin is None:
Gmin = 0
if Ntran is None:
Ntran = 0
# time variable component: Tt this is always the same in each iter. So we
# build it once for all.
Tt = np.zeros((mna_size, 1))
v_eq = 0
if not skip_Tt:
for elem in circ:
if (isinstance(elem, components.sources.VSource) or isinstance(elem, components.sources.ISource)) and elem.is_timedependent:
if isinstance(elem, components.sources.VSource):
Tt[nv - 1 + v_eq, 0] = -1 * elem.V(time)
elif isinstance(elem, components.sources.ISource):
if elem.n1:
Tt[elem.n1 - 1, 0] = Tt[elem.n1 - 1, 0] + elem.I(time)
if elem.n2:
Tt[elem.n2 - 1, 0] = Tt[elem.n2 - 1, 0] - elem.I(time)
if circuit.is_elem_voltage_defined(elem):
v_eq = v_eq + 1
# update N to include the time variable sources
Ndc = Ndc + Tt
# initial guess, if specified, otherwise it's zero
if x0 is not None:
if isinstance(x0, results.op_solution):
x = x0.asarray()
else:
x = x0
else:
x = np.zeros((mna_size, 1))
# has n-1 rows because of discard of ^^^
converged = False
standard_solving, gmin_stepping, source_stepping = get_solve_methods()
standard_solving, gmin_stepping, source_stepping = set_next_solve_method(
standard_solving, gmin_stepping,
source_stepping, verbose)
convergence_by_node = None
printing.print_info_line(("Solving... ", 3), verbose, print_nl=False)
while(not converged):
if standard_solving["enabled"]:
mna_to_pass = mna + Gmin
N_to_pass = Ndc + Ntran * (Ntran is not None)
elif gmin_stepping["enabled"]:
# print "gmin index:", str(gmin_stepping["index"])+", gmin:", str(
# 10**(gmin_stepping["factors"][gmin_stepping["index"]]))
printing.print_info_line(
("Setting Gmin to: " + str(10 ** gmin_stepping["factors"][gmin_stepping["index"]]), 6), verbose)
mna_to_pass = build_gmin_matrix(
circ, 10 ** (gmin_stepping["factors"][gmin_stepping["index"]]), mna_size, verbose) + mna
N_to_pass = Ndc + Ntran * (Ntran is not None)
elif source_stepping["enabled"]:
printing.print_info_line(
("Setting sources to " + str(source_stepping["factors"][source_stepping["index"]] * 100) + "% of their actual value", 6), verbose)
mna_to_pass = mna + Gmin
N_to_pass = source_stepping["factors"][source_stepping["index"]]*Ndc + Ntran*(Ntran is not None)
try:
(x, error, converged, n_iter, convergence_by_node) = mdn_solver(x, mna_to_pass, circ, T=N_to_pass,
nv=nv, print_steps=(verbose > 0), locked_nodes=locked_nodes, time=time, MAXIT=MAXIT, debug=(verbose == 6))
tot_iterations += n_iter
except np.linalg.linalg.LinAlgError:
n_iter = 0
converged = False
print("failed.")
printing.print_general_error("J Matrix is singular")
except OverflowError:
n_iter = 0
converged = False
print("failed.")
printing.print_general_error("Overflow")
if not converged:
if verbose == 6 and convergence_by_node is not None:
for ivalue in range(len(convergence_by_node)):
if not convergence_by_node[ivalue] and ivalue < nv - 1:
print("Convergence problem node %s" % (circ.int_node_to_ext(ivalue),))
elif not convergence_by_node[ivalue] and ivalue >= nv - 1:
e = circ.find_vde(ivalue)
print("Convergence problem current in %s" % e.part_id)
if n_iter == MAXIT - 1:
printing.print_general_error(
"Error: MAXIT exceeded (" + str(MAXIT) + ")")
if more_solve_methods_available(standard_solving, gmin_stepping, source_stepping):
standard_solving, gmin_stepping, source_stepping = set_next_solve_method(
standard_solving, gmin_stepping, source_stepping, verbose)
else:
# print "Giving up."
x = None
error = None
break
else:
printing.print_info_line(
("[%d iterations]" % (n_iter,), 6), verbose)
if (source_stepping["enabled"] and source_stepping["index"] != 9):
converged = False
source_stepping["index"] = source_stepping["index"] + 1
elif (gmin_stepping["enabled"] and gmin_stepping["index"] != 9):
gmin_stepping["index"] = gmin_stepping["index"] + 1
converged = False
else:
printing.print_info_line((" done.", 3), verbose)
return (x, error, converged, tot_iterations)
[docs]def build_gmin_matrix(circ, gmin, mna_size, verbose):
"""Build a Gmin matrix
**Parameters:**
circ : circuit instance
The circuit for which the matrix is built.
gmin : scalar float
The value of the minimum conductance to ground to be used.
mna_size : int
The size of the MNA matrix associated with the GMIN matrix being built.
verbose : int
The verbosity level, from 0 (silent) to 6 (debug).
**Returns:**
Gmin : ndarray of size (mna_size, mna_size)
The Gmin matrix itself.
"""
printing.print_info_line(("Building Gmin matrix...", 5), verbose)
Gmin_matrix = np.zeros((mna_size, mna_size))
for index in range(circ.get_nodes_number() - 1):
Gmin_matrix[index, index] = gmin
# the three missing terms of the stample matrix go on [index,0] [0,0] [0, index] but since
# we discarded the 0 row and 0 column, we simply don't need to add them
# the last lines are the KVL lines, introduced by voltage sources.
# Don't add gmin there.
return Gmin_matrix
[docs]def set_next_solve_method(standard_solving, gmin_stepping, source_stepping, verbose=3):
"""Select the next solving method.
We have the standard solving method and two homotopies available. The
homotopies are :math:`G_{min}` stepping and source stepping.
They will be selected and enabled when failures occur according to the
options values:
* ``options.use_standard_solve_method``,
* ``options.use_gmin_stepping``,
* ``options.use_source_stepping``.
The methods will be used in the order above.
The inputs to this method are three dictionaries that keep track of which
method is currently enabled and which ones has failed in the past.
**Parameters:**
standard_solving, gmin_stepping, source_stepping : dict
The dictionaries contain the options and the status of the methods, they
should be the values provided by :func:`get_solve_methods`.
verbose : int, optional
The verbosity level, from 0 (silent) to 6 (debug).
**Returns:**
standard_solving, gmin_stepping, source_stepping : dict
The updated dictionaries.
"""
if standard_solving["enabled"]:
printing.print_info_line(("failed.", 1), verbose)
standard_solving["enabled"] = False
standard_solving["failed"] = True
elif gmin_stepping["enabled"]:
printing.print_info_line(("failed.", 1), verbose)
gmin_stepping["enabled"] = False
gmin_stepping["failed"] = True
elif source_stepping["enabled"]:
printing.print_info_line(("failed.", 1), verbose)
source_stepping["enabled"] = False
source_stepping["failed"] = True
if not standard_solving["failed"] and options.use_standard_solve_method:
standard_solving["enabled"] = True
elif not gmin_stepping["failed"] and options.use_gmin_stepping:
gmin_stepping["enabled"] = True
printing.print_info_line(
("Enabling gmin stepping convergence aid.", 3), verbose)
elif not source_stepping["failed"] and options.use_source_stepping:
source_stepping["enabled"] = True
printing.print_info_line(
("Enabling source stepping convergence aid.", 3), verbose)
return standard_solving, gmin_stepping, source_stepping
[docs]def more_solve_methods_available(standard_solving, gmin_stepping, source_stepping):
"""Are there more solving methods available?
**Parameters:**
standard_solving, gmin_stepping, source_stepping : dict
The dictionaries contain the options and the status of the methods.
**Returns:**
rsp : boolean
The answer.
"""
if (standard_solving["failed"] or not options.use_standard_solve_method) and \
(gmin_stepping["failed"] or not options.use_gmin_stepping) and \
(source_stepping["failed"] or not options.use_source_stepping):
return False
else:
return True
[docs]def get_solve_methods():
"""Get all the available solving methods
We have the standard solving method and two homotopies available. The
homotopies are :math:`G_{min}` stepping and source stepping.
Solving methods may be enabled and disabled through the
options values:
* ``options.use_standard_solve_method``,
* ``options.use_gmin_stepping``,
* ``options.use_source_stepping``.
**Returns:**
standard_solving, gmin_stepping, source_stepping : dict
The dictionaries contain the options and the status of the methods.
"""
standard_solving = {"enabled": False, "failed": False}
g_indices = list(range(int(numpy.log(options.gmin)), 0))
g_indices.reverse()
gmin_stepping = {"enabled": False, "failed":
False, "factors": g_indices, "index": 0}
source_stepping = {"enabled": False, "failed": False, "factors": (
0.001, .005, .01, .03, .1, .3, .5, .7, .8, .9), "index": 0}
return standard_solving, gmin_stepping, source_stepping
[docs]def dc_analysis(circ, start, stop, step, source, sweep_type='LINEAR', guess=True, x0=None, outfile="stdout", verbose=3):
"""Performs a sweep of the value of V or I of a independent source from start
value to stop value using the provided step.
For every circuit generated, computes the OP. This function relays on
:func:`dc_analysis.op_analysis` to actually solve each circuit.
**Parameters:**
circ : Circuit instance
The circuit instance to be simulated.
start : float
Start value of the sweep source
stop : float
Stop value of the sweep source
step : float
The step size in the sweep
source : string
The part ID of the source to be swept, eg. ``'V1'``.
sweep_type : string, optional
Either options.dc_lin_step (default) or options.dc_log_step
guess : boolean, optional
op_analysis will guess to start the first NR iteration for the first point,
the previsious OP is used from then on. Defaults to ``True``.
outfile : string, optional
Filename of the output file. If set to ``'stdout'`` (default), prints to
screen.
verbose : int
The verbosity level, from 0 (silent) to 6 (debug).
**Returns:**
rstdc : results.dc_solution instance or None
A ``results.dc_solution`` instance is returned, if a solution was found
for at least one sweep value. or ``None``, if an error occurred (eg
invalid start/stop/step values) or there was no solution for any
sweep value.
"""
if outfile == 'stdout':
verbose = 0
printing.print_info_line(("Starting DC analysis:", 2), verbose)
elem_type, elem_descr = source[0].lower(), source.lower() # eg. 'v', 'v34'
sweep_label = elem_type[0].upper() + elem_descr[1:]
if sweep_type == options.dc_log_step and stop - start < 0:
printing.print_general_error(
"DC analysis has log sweeping and negative stepping.")
sys.exit(1)
if (stop - start) * step < 0:
raise ValueError("Unbonded stepping in DC analysis.")
points = (stop - start) / step + 1
sweep_type = sweep_type.upper()[:3]
if sweep_type == options.dc_log_step:
dc_iter = utilities.log_axis_iterator(start, stop, points=points)
elif sweep_type == options.dc_lin_step:
dc_iter = utilities.lin_axis_iterator(start, stop, points=points)
else:
printing.print_general_error("Unknown sweep type: %s" % (sweep_type,))
sys.exit(1)
if elem_type != 'v' and elem_type != 'i':
printing.print_general_error(
"Sweeping is possible only with voltage and current sources. (" + str(elem_type) + ")")
sys.exit(1)
source_elem = None
for index in range(len(circ)):
if circ[index].part_id.lower() == elem_descr:
if elem_type == 'v':
if isinstance(circ[index], components.sources.VSource):
source_elem = circ[index]
break
if elem_type == 'i':
if isinstance(circ[index], components.sources.ISource):
source_elem = circ[index]
break
if not source_elem:
raise ValueError(".DC: source %s was not found." % source)
if isinstance(source_elem, components.sources.VSource):
initial_value = source_elem.dc_value
else:
initial_value = source_elem.dc_value
# If the initial value is set to None, op_analysis will attempt a smart guess (if guess),
# Then for each iteration, the last result is used as x0, since op_analysis will not
# attempt to guess the op if x0 is not None.
x = x0
sol = results.dc_solution(
circ, start, stop, sweepvar=sweep_label, stype=sweep_type, outfile=outfile)
printing.print_info_line(("Solving... ", 3), verbose, print_nl=False)
tick = ticker.ticker(1)
tick.display(verbose > 2)
# sweep setup
# tarocca il generatore di tensione, avvia DC silenziosa, ritarocca etc
index = 0
for sweep_value in dc_iter:
index = index + 1
if isinstance(source_elem, components.sources.VSource):
source_elem.dc_value = sweep_value
else:
source_elem.dc_value = sweep_value
# silently calculate the op
x = op_analysis(circ, x0=x, guess=guess, verbose=0)
if x is None:
tick.hide(verbose > 2)
if not options.dc_sweep_skip_allowed:
print("Could't solve the circuit for sweep value:", start + index * step)
solved = False
break
else:
print("Skipping sweep value:", start + index * step)
continue
solved = True
sol.add_op(sweep_value, x)
tick.step()
tick.hide(verbose > 2)
if solved:
printing.print_info_line(("done", 3), verbose)
# clean up
if isinstance(source_elem, components.sources.VSource):
source_elem.dc_value = initial_value
else:
source_elem.dc_value = initial_value
return sol if solved else None
[docs]def op_analysis(circ, x0=None, guess=True, outfile=None, verbose=3):
"""Runs an Operating Point (OP) analysis
**Parameters:**
circ : Circuit instance
The circuit instance on which the simulation is run
x0 : op_solution instance or ndarray, optional
The initial guess to be used to start the NR :func:`mdn_solver`.
guess : boolean, optional
If set to ``True`` (default) and ``x0`` is ``None``, it will generate a
'smart' guess to use as ``x0``.
verbose : int
The verbosity level from 0 (silent) to 6 (debug).
**Returns:**
A ``result.op_solution`` instance, if successful, ``None`` otherwise.
"""
if outfile == 'stdout':
verbose = 0 # silent mode, print out results only.
if not options.dc_use_guess:
guess = False
(mna, N) = generate_mna_and_N(circ, verbose=verbose)
printing.print_info_line(
("MNA matrix and constant term (complete):", 4), verbose)
printing.print_info_line((mna, 4), verbose)
printing.print_info_line((N, 4), verbose)
# lets trash the unneeded col & row
printing.print_info_line(
("Removing unneeded row and column...", 4), verbose)
mna = utilities.remove_row_and_col(mna)
N = utilities.remove_row(N, rrow=0)
printing.print_info_line(("Starting op analysis:", 2), verbose)
if x0 is None and guess:
x0 = dc_guess.get_dc_guess(circ, verbose=verbose)
# if x0 is not None, use that
printing.print_info_line(("Solving with Gmin:", 4), verbose)
Gmin_matrix = build_gmin_matrix(
circ, options.gmin, mna.shape[0], verbose - 2)
(x1, error1, solved1, n_iter1) = dc_solve(mna, N,
circ, Gmin=Gmin_matrix, x0=x0, verbose=verbose)
# We'll check the results now. Recalculate them without Gmin (using previsious solution as initial guess)
# and check that differences on nodes and current do not exceed the
# tolerances.
if solved1:
op1 = results.op_solution(
x1, error1, circ, outfile=outfile, iterations=n_iter1)
printing.print_info_line(("Solving without Gmin:", 4), verbose)
(x2, error2, solved2, n_iter2) = dc_solve(
mna, N, circ, Gmin=None, x0=x1, verbose=verbose)
else:
solved2 = False
if solved1 and not solved2:
printing.print_general_error("Can't solve without Gmin.")
if verbose:
print("Displaying latest valid results.")
op1.write_to_file(filename='stdout')
opsolution = op1
elif solved1 and solved2:
op2 = results.op_solution(
x2, error2, circ, outfile=outfile, iterations=n_iter1 + n_iter2)
op2.gmin = 0
badvars = results.op_solution.gmin_check(op2, op1)
printing.print_result_check(badvars, verbose=verbose)
check_ok = not (len(badvars) > 0)
if not check_ok and verbose:
print("Solution with Gmin:")
op1.write_to_file(filename='stdout')
print("Solution without Gmin:")
op2.write_to_file(filename='stdout')
opsolution = op2
else: # not solved1
printing.print_general_error("Couldn't solve the circuit. Giving up.")
opsolution = None
if opsolution and outfile != 'stdout' and outfile is not None:
opsolution.write_to_file()
if opsolution and (verbose > 2 or outfile == 'stdout') and options.cli:
opsolution.write_to_file(filename='stdout')
return opsolution
[docs]def mdn_solver(x, mna, circ, T, MAXIT, nv, locked_nodes, time=None,
print_steps=False, vector_norm=lambda v: max(abs(v)),
debug=True):
"""
Solves a problem like F(x) = 0 using the Newton Algorithm with a variable
damping.
Where:
.. math::
F(x) = mna*x + T + T(x)
* :math:`mna` is the Modified Network Analysis matrix of the circuit
* :math:`T(x)` is the contribute of nonlinear elements to KCL
* :math:`T` contains the contributions of the independent sources, time
* invariant and linear
If :math:`x(0)` is the initial guess, every :math:`x(n+1)` is given by:
.. math::
x(n+1) = x(n) + td \\cdot dx
Where :math:`td` is a damping coefficient to avoid overflow in non-linear
components and excessive oscillation in the very first iteration. Afterwards
:math:`td=1` To calculate :math:`td`, an array of locked nodes is needed.
The convergence check is done this way:
**Parameters:**
x : ndarray
The initial guess. If set to ``None``, it will be initialized to all
zeros. Specifying a initial guess may improve the convergence time of
the algorithm and determine which solution (if any) is found if there
are more than one.
mna : ndarray
The Modified Network Analysis matrix of the circuit, reduced, see above.
circ : circuit instance
The circuit instance.
T : ndarray,
The :math:`T` vector described above.
MAXIT : int
The maximum iterations that the method may perform.
nv : int
Number of nodes in the circuit (counting the ref, 0)
locked_nodes : list of tuples
A list of ports driving non-linear elements, generated by
``circ.get_locked_nodes()``
time : float or None, optional
The value of time to be passed to non_linear _and_ time variant
elements.
print_steps : boolean, optional
Show a progress indicator, very verbose. Defaults to ``False``.
vector_norm : function, optional
An R^N -> R^1 function returning the norm of a vector, for convergence
checking. Defaults to the maximum norm, ie :math:`f(x) = max(|x|)`,
debug : int, optional
Debug flag that will result in an array being returned containing
node-by-node convergence information.
**Returns:**
sol : ndarray
The solution.
err : ndarray
The remaining error.
converged : boolean
A boolean that is set to ``True`` whenever the method exits because of a
successful convergence check. ``False`` whenever convergence problems
where found.
N : int
The number of NR iterations performed.
convergence_by_node : list
If ``debug`` was set to ``True``, this list has the same size of the MNA
matrix and contains the information regarding which nodes fail to
converge in the circuit. Ie. ``if convergence_by_node[j] == False``,
node ``j`` has a convergence problem. This may significantly help
debugging non-convergent circuits.
"""
# OLD COMMENT: FIXME REWRITE: solve through newton
# problem is F(x)= mna*x +H(x) = 0
# H(x) = N + T(x)
# lets say: J = dF/dx = mna + dT(x)/dx
# J*dx = -1*(mna*x+N+T(x))
# dT/dx e' lo jacobiano -> g_eq (o gm)
# print_steps = False
# locked_nodes = get_locked_nodes(element_list)
mna_size = mna.shape[0]
nonlinear_circuit = circ.is_nonlinear()
tick = ticker.ticker(increments_for_step=1)
tick.display(print_steps)
if x is None:
# if no guess was specified, its all zeros
x = np.zeros((mna_size, 1))
else:
if x.shape[0] != mna_size:
raise ValueError("x0s size is different from expected: got "
"%d-elements x0 with an MNA of size %d" %
(x.shape[0], mna_size))
if T is None:
printing.print_warning(
"dc_analysis.mdn_solver called with T==None, setting T=0. BUG or no sources in circuit?")
T = np.zeros((mna_size, 1))
sparse = mna_size > options.dense_matrix_limit
# We allocate the matrices once and then reuse them
if sparse:
mna = scipy.sparse.coo_matrix(mna)
J = scipy.sparse.lil_matrix((mna_size, mna_size))
else:
J = np.zeros((mna_size, mna_size))
Tx = np.zeros((mna_size, 1))
converged = False
iteration = 0
while iteration < MAXIT: # newton iteration counter
iteration += 1
tick.step()
if nonlinear_circuit:
# build dT(x)/dx (stored in J) and Tx(x)
J[:, :] = 0.0
Tx[:, 0] = 0.0
for elem in circ:
if elem.is_nonlinear:
_update_J_and_Tx(J, Tx, x, elem, time)
residuo = mna.dot(x) + T + nonlinear_circuit*Tx
if sparse:
lu = scipy.sparse.linalg.splu(scipy.sparse.csc_matrix(mna + nonlinear_circuit*J))
dx = lu.solve(-residuo)
else:
dx = np.linalg.solve(mna + nonlinear_circuit*J, -residuo)
x = x + get_td(dx, locked_nodes, n=iteration) * dx
if not nonlinear_circuit:
converged = True
break
elif convergence_check(x, dx, residuo, nv - 1)[0]:
converged = True
break
# if vector_norm(dx) == np.nan: #Overflow
# raise OverflowError
tick.hide(print_steps)
if debug and not converged:
# re-run the convergence check, only this time get the results
# by node, so we can show to the users which nodes are misbehaving.
converged, convergence_by_node = convergence_check(
x, dx, residuo, nv - 1, debug=True)
else:
convergence_by_node = []
return (x, residuo, converged, iteration, convergence_by_node)
def _update_J_and_Tx(J, Tx, x, elem, time):
out_ports = elem.get_output_ports()
for index in range(len(out_ports)):
n1, n2 = out_ports[index]
n1m1, n2m1 = n1 - 1, n2 - 1
dports = elem.get_drive_ports(index)
v_dports = []
for port in dports:
v = 0. # build v: remember we removed the 0 row and 0 col of mna -> -1
if port[0]:
v = v + x[port[0] - 1, 0]
if port[1]:
v = v - x[port[1] - 1, 0]
v_dports.append(v)
if hasattr(elem, 'gstamp') and hasattr(elem, 'istamp'):
iis, gs = elem.gstamp(v_dports, time)
J[iis] += gs.reshape(-1)
iis, i = elem.istamp(v_dports, time)
Tx[iis] += i.reshape(-1)
continue
if n1 or n2:
iel = elem.i(index, v_dports, time)
if n1:
Tx[n1m1, 0] = Tx[n1m1, 0] + iel
if n2:
Tx[n2m1, 0] = Tx[n2m1, 0] - iel
for iindex in range(len(dports)):
if n1 or n2:
g = elem.g(index, v_dports, iindex, time)
if n1:
if dports[iindex][0]:
J[n1m1, dports[iindex][0] - 1] += g
if dports[iindex][1]:
J[n1m1, dports[iindex][1] - 1] -= g
if n2:
if dports[iindex][0]:
J[n2m1, dports[iindex][0] - 1] -= g
if dports[iindex][1]:
J[n2m1, dports[iindex][1] - 1] += g
[docs]def get_td(dx, locked_nodes, n=-1):
"""Calculates the damping coefficient for the Newthon method.
The damping coefficient is choosen as the lowest between:
- the damping required for the first NR iterations, a parameter which is set
through the integer ``options.nr_damp_first_iters``.
- If ``options.nl_voltages_lock`` evaluates to ``True``, the biggest damping
factor that keeps the change in voltage across the locked nodes pairs less
than the maximum variation allowed, set by:
``(options.nl_voltages_lock_factor * Vth)``
- Unity.
**Parameters:**
dx : ndarray
The undamped increment returned by the NR solver.
locked_nodes : list
A vector of tuples of (internal) nodes that are a port of a non-linear
component.
n : int, optional
The NR iteration counter
.. note::
If ``n`` is set to ``-1`` (or any negative value), ``td`` is independent
from the iteration number and ``options.nr_damp_first_iters`` is ignored.
**Returns:**
td : float
The damping coefficient.
"""
if not options.nr_damp_first_iters or n < 0:
td = 1
else:
if n < 10:
td = 1e-2
elif n < 20:
td = 0.1
else:
td = 1
td_new = 1
if options.nl_voltages_lock:
for (n1, n2) in locked_nodes:
if n1 != 0:
if n2 != 0:
if abs(dx[n1 - 1, 0] - dx[n2 - 1, 0]) > options.nl_voltages_lock_factor * constants.Vth():
td_new = (options.nl_voltages_lock_factor * constants.Vth()) / abs(
dx[n1 - 1, 0] - dx[n2 - 1, 0])
else:
if abs(dx[n1 - 1, 0]) > options.nl_voltages_lock_factor * constants.Vth():
td_new = (options.nl_voltages_lock_factor * constants.Vth()) / abs(
dx[n1 - 1, 0])
else:
if abs(dx[n2 - 1, 0]) > options.nl_voltages_lock_factor * constants.Vth():
td_new = (options.nl_voltages_lock_factor * constants.Vth()) / abs(
dx[n2 - 1, 0])
if td_new < td:
td = td_new
return td
[docs]def generate_mna_and_N(circ, verbose=3):
"""Generate the full *unreduced* MNA and N matrices required for an MNA analysis
We wish to solve the linear stationary MNA problem:
.. math::
MNA \\cdot x + N = 0
If ``nv`` is the number of nodes in the circuit, ``MNA`` is a square matrix
composed by:
* ``MNA[:nv, :]``, KCLs ordered by node, from node 0 up to node nv.
In the above submatrix, we have a voltage part: ``MNA[:nv, :nv]``, where
each term ``MNA[i, j]`` is due to the (trans-)conductances in between the
nodes and a current part, ``MNA[:nv, nv:]``, where each term is due to a
current variable introduced by elements whose current flow is not univocally
defined by the voltage applied to their port(s).
* ``MNA[nv:, :]`` are the KVL equations introduced by the above terms.
``N`` is similarly partitioned, but it is a vector of size ``(nv,)``.
**Parameters:**
circ : circuit instance
The circuit for which the matrices are to be computed.
verbose : int, optional
The verbosity, from 0 (silent) to 6 (debug).
**Returns:**
MNA, N : ndarrays
The MNA matrix and constant term vector computed as per above.
"""
n_of_nodes = circ.get_nodes_number()
mna = np.zeros((n_of_nodes, n_of_nodes))
N = np.zeros((n_of_nodes, 1))
for elem in circ:
if elem.is_nonlinear:
continue
elif isinstance(elem, components.Resistor):
mna[elem.n1, elem.n1] = mna[elem.n1, elem.n1] + elem.g
mna[elem.n1, elem.n2] = mna[elem.n1, elem.n2] - elem.g
mna[elem.n2, elem.n1] = mna[elem.n2, elem.n1] - elem.g
mna[elem.n2, elem.n2] = mna[elem.n2, elem.n2] + elem.g
elif isinstance(elem, components.Capacitor):
pass # In a capacitor I(V) = 0
elif isinstance(elem, components.sources.GISource):
mna[elem.n1, elem.sn1] = mna[elem.n1, elem.sn1] + elem.alpha
mna[elem.n1, elem.sn2] = mna[elem.n1, elem.sn2] - elem.alpha
mna[elem.n2, elem.sn1] = mna[elem.n2, elem.sn1] - elem.alpha
mna[elem.n2, elem.sn2] = mna[elem.n2, elem.sn2] + elem.alpha
elif isinstance(elem, components.sources.ISource):
if not elem.is_timedependent: # convenzione normale!
N[elem.n1, 0] = N[elem.n1, 0] + elem.I()
N[elem.n2, 0] = N[elem.n2, 0] - elem.I()
else:
pass # vengono aggiunti volta per volta
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 isinstance(elem, components.sources.FISource):
# we add these last, they depend on voltage sources
# to sense the current
pass
else:
print("dc_analysis.py: BUG - Unknown linear element. Ref. #28934")
# process vsources
# i generatori di tensione non sono pilotabili in tensione: g e' infinita
# for each vsource, introduce a new variable: the current flowing through it.
# then we introduce a KVL equation to be able to solve the circuit
for elem in circ:
if circuit.is_elem_voltage_defined(elem):
index = mna.shape[0] # get_matrix_size(mna)[0]
mna = utilities.expand_matrix(mna, add_a_row=True, add_a_col=True)
N = utilities.expand_matrix(N, add_a_row=True, add_a_col=False)
# KCL
mna[elem.n1, index] = 1.0
mna[elem.n2, index] = -1.0
# KVL
mna[index, elem.n1] = +1.0
mna[index, elem.n2] = -1.0
if isinstance(elem, components.sources.VSource) and not elem.is_timedependent:
# corretto, se e' def una parte tempo-variabile ci pensa
# mdn_solver a scegliere quella giusta da usare.
N[index, 0] = -1.0 * elem.V()
elif isinstance(elem, components.sources.VSource) and elem.is_timedependent:
pass # taken care step by step
elif isinstance(elem, components.sources.EVSource):
mna[index, elem.sn1] = -1.0 * elem.alpha
mna[index, elem.sn2] = +1.0 * elem.alpha
elif isinstance(elem, components.Inductor):
# N[index,0] = 0 pass, it's already zero
pass
elif isinstance(elem, components.sources.HVSource):
index_source = circ.find_vde_index(elem.source_id)
mna[index, n_of_nodes+index_source] = 1.0 * elem.alpha
else:
print("dc_analysis.py: BUG - found an unknown voltage_def elem.")
print(elem)
sys.exit(33)
# iterate again for devices that depend on voltage-defined ones.
for elem in circ:
if isinstance(elem, components.sources.FISource):
local_i_index = circ.find_vde_index(elem.source_id, verbose=0)
mna[elem.n1, n_of_nodes + local_i_index] = mna[elem.n1, n_of_nodes + local_i_index] + elem.alpha
mna[elem.n2, n_of_nodes + local_i_index] = mna[elem.n2, n_of_nodes + local_i_index] - elem.alpha
# Seems a good place to run some sanity check
# for the time being we do not halt the execution
utilities.check_ground_paths(mna, circ, reduced_mna=False, verbose=verbose)
# all done
return (mna, N)
[docs]def build_x0_from_user_supplied_ic(circ, icdict):
"""Builds a vector of appropriate (reduced!) size from the values supplied
in ``icdict``.
Supplying a custom x0 can be useful:
- To aid convergence in tough circuits,
- To start a transient simulation from a particular x0.
**Parameters:**
circ: circuit instance
The circuit the :math:`x_0` is being assembled for
icdict: dict
``icdict`` is a a dictionary assembled as follows:
- to specify a nodal voltage: ``{'V(node)':<voltage value>}``
Eg. ``{'V(n1)':2.3, 'V(n2)':0.45, ...}``.
All unspecified voltages default to 0.
- to specify a branch current: ``'I(<element>)':<current value>}``
ie. the elements names are sorrounded by ``I(...)``.
Eg. ``{'I(L1)':1.03e-3, I(V4):2.3e-6, ...}``
All unspecified currents default to 0.
Notes: this simulator uses the standard convention.
**Returns:**
x0 : ndarray
The x0 matrix assembled according to ``icdict``.
:raises ValueError: whenever a malformed ``icdict`` is supplied.
"""
Vregex = re.compile("V\s*\(\s*([a-z0-9]+)\s*\)", re.IGNORECASE | re.DOTALL)
Iregex = re.compile("I\s*\(\s*([a-z0-9]+)\s*\)", re.IGNORECASE | re.DOTALL)
nv = circ.get_nodes_number() # number of voltage variables
voltage_defined_elem_names = \
[elem.part_id.lower() for elem in circ
if circuit.is_elem_voltage_defined(elem)]
ni = len(voltage_defined_elem_names) # number of current variables
x0 = np.zeros((nv + ni, 1))
for label in icdict.keys():
value = icdict[label]
if Vregex.search(label):
ext_node = Vregex.findall(label)[0]
int_node = circ.ext_node_to_int(ext_node)
x0[int_node, 0] = value
elif Iregex.search(label):
element_name = Iregex.findall(label)[0]
index = voltage_defined_elem_names.index(element_name.lower())
x0[nv + index, 0] = value
else:
raise ValueError("Unrecognized label " + label)
return x0[1:, :]
[docs]def modify_x0_for_ic(circ, x0):
"""Modifies a supplied x0.
Several circut elements allow the user to set their own Initial
Conditions (IC) for either voltage or current, depending on what
is appropriate for the element considered.
This method, receives a preliminary ``x0`` value, typically computed
by an OP analysis and goes through the circuit, looking for ICs and
setting them in ``x0``.
Notice it is possible to require ICs that are incompatible with each
other -- for example supplying different ICs to two parallel caps.
In that case we try to accommodate the user's requirements in a
non-strict best-effort kind of way: for this reason, whenever
multiple ICs are specified, it is best to visually inspect ``x0``
to check that what you would have expected is indeed what you got.
**Parameters**
circ : circuit instance
The circuit in which the ICs are specified.
x0 : ndarray or results.op_solution
The initial value to be modified
**Returns:**
x0p : ndarray or results.op_solution
The modified ``x0``. Notice that we return the same
kind of object as it was supplied. Additionally,
the ``results.op_solution`` is a *new* *instance*,
while the ``ndarray`` is simply the original array
modified.
"""
if isinstance(x0, results.op_solution):
x0 = copy.copy(x0.asarray())
return_obj = True
else:
return_obj = False
nv = circ.get_nodes_number() # number of voltage variables
voltage_defined_elements = [
x for x in circ if circuit.is_elem_voltage_defined(x)]
# setup voltages this may _not_ work properly
for elem in circ:
if isinstance(elem, components.Capacitor) and elem.ic or \
isinstance(elem, diode.diode) and elem.ic:
x0[elem.n1 - 1, 0] = x0[elem.n2 - 1, 0] + elem.ic
# setup the currents
for elem in voltage_defined_elements:
if isinstance(elem, components.Inductor) and elem.ic:
x0[nv - 1 + voltage_defined_elements.index(elem), 0] = elem.ic
if return_obj:
xnew = results.op_solution(x=x0, \
error=np.zeros(x0.shape), circ=circ, outfile=None)
xnew.netlist_file = None
xnew.netlist_title = "Self-generated OP to be used as tran IC"
else:
xnew = x0
return xnew