Source code for ahkab.transient

# -*- coding: iso-8859-1 -*-
# transient.py
# Transient analysis
# 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 methods required to perform a transient analysis.

Our problem can be written as:

.. math::

    D \\cdot dx/dt + MNA \\cdot x + T_v(x) + T_t(t) + N = 0

We need:

    1. :math:`MNA`, the static Modified Nodal Analysis matrix,
    2. :math:`N`, constant DC term,
    3. :math:`T_v(x)`, the non-linear DC term
    4. :math:`T_t(t)`, the time variant term, time dependent-sources,
       to be evaluated at each time step,
    5. The dynamic :math:`D` matrix,
    6. a differentiation method to approximate :math:`dx/dt`.

"""

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

import sys
import imp

import numpy as np

from . import dc_analysis
from . import implicit_euler
from . import ticker
from . import options
from . import circuit
from . import printing
from . import utilities
from . import components
from . import results

# differentiation methods, add them here
IMPLICIT_EULER = "IMPLICIT_EULER"
TRAP = "TRAP"
GEAR1 = "GEAR1"
GEAR2 = "GEAR2"
GEAR3 = "GEAR3"
GEAR4 = "GEAR4"
GEAR5 = "GEAR5"
GEAR6 = "GEAR6"

specs = {'tran':{'tokens':({
                          'label':'tstep',
                          'pos':0,
                          'type':float,
                          'needed':True,
                          'dest':'tstep',
                          'default':None
                         },
                         {
                          'label':'tstop',
                          'pos':1,
                          'type':float,
                          'needed':True,
                          'dest':'tstop',
                          'default':None
                         },
                         {
                          'label':'tstart',
                          'pos':None,
                          'type':float,
                          'needed':False,
                          'dest':'tstart',
                          'default':0
                         },
                         {
                          'label':'uic',
                          'pos':2,
                          'type':float,
                          'needed':False,
                          'dest':'uic',
                          'default':0
                         },
                         {
                          'label':'ic_label',
                          'pos':None,
                          'type':str,
                          'needed':False,
                          'dest':'x0',
                          'default':0
                         },
                         {
                          'label':'method',
                          'pos':None,
                          'type':str,
                          'needed':False,
                          'dest':'method',
                          'default':None
                         }
                        )
               }
           }


[docs]def transient_analysis(circ, tstart, tstep, tstop, method=options.default_tran_method, use_step_control=True, x0=None, mna=None, N=None, D=None, outfile="stdout", return_req_dict=None, verbose=3): """Performs a transient analysis of the circuit described by circ. Parameters: circ: circuit instance to be simulated. tstart: start value. Better leave this to zero. tstep: the maximum step to be allowed during simulation or tstop: stop value for simulation method: differentiation method: 'TRAP' (default) or 'IMPLICIT_EULER' or 'GEARx' with x=1..6 use_step_control: the LTE will be calculated and the step adjusted. default: True x0: the starting point, the solution at t=tstart (defaults to None, will be set to the OP) mna, N, D: MNA matrices, defaulting to None, for big circuits, reusing matrices saves time outfile: filename, the results will be written to this file. "stdout" means print out. return_req_dict: to be documented verbose: verbosity level from 0 (silent) to 6 (very verbose). """ if outfile == "stdout": verbose = 0 _debug = False if options.transient_no_step_control: use_step_control = False if _debug: print_step_and_lte = True else: print_step_and_lte = False method = method.upper() if method is not None else options.default_tran_method HMAX = tstep #check parameters if tstart > tstop: printing.print_general_error("tstart > tstop") sys.exit(1) if tstep < 0: printing.print_general_error("tstep < 0") sys.exit(1) if verbose > 4: tmpstr = "Vea = %g Ver = %g Iea = %g Ier = %g max_time_iter = %g HMIN = %g" % \ (options.vea, options.ver, options.iea, options.ier, options.transient_max_time_iter, options.hmin) printing.print_info_line((tmpstr, 5), verbose) locked_nodes = circ.get_locked_nodes() if print_step_and_lte: flte = open("step_and_lte.graph", "w") flte.write("#T\tStep\tLTE\n") printing.print_info_line(("Starting transient analysis: ", 3), verbose) printing.print_info_line(("Selected method: %s" % (method,), 3), verbose) #It's a good idea to call transient with prebuilt MNA and N matrix #the analysis will be slightly faster (long netlists). if mna is None or N is None: (mna, N) = dc_analysis.generate_mna_and_N(circ, verbose=verbose) mna = utilities.remove_row_and_col(mna) N = utilities.remove_row(N, rrow=0) elif not mna.shape[0] == N.shape[0]: printing.print_general_error("mna matrix and N vector have different number of columns.") sys.exit(0) if D is None: # if you do more than one tran analysis, output streams should be changed... # this needs to be fixed D = generate_D(circ, (mna.shape[0], mna.shape[0])) D = utilities.remove_row_and_col(D) # setup x0 if x0 is None: printing.print_info_line(("Generating x(t=%g) = 0" % (tstart,), 5), verbose) x0 = np.zeros((mna.shape[0], 1)) opsol = results.op_solution(x=x0, error=x0, circ=circ, outfile=None) else: if isinstance(x0, results.op_solution): opsol = x0 x0 = x0.asarray() else: opsol = results.op_solution(x=x0, error=np.zeros((mna.shape[0], 1)), circ=circ, outfile=None) printing.print_info_line(("Using the supplied op as x(t=%g)." % (tstart,), 5), verbose) if verbose > 4: print("x0:") opsol.print_short() # setup the df method printing.print_info_line(("Selecting the appropriate DF ("+method+")... ", 5), verbose, print_nl=False) if method == IMPLICIT_EULER: from . import implicit_euler as df elif method == TRAP: from . import trap as df elif method == GEAR1: from . import gear as df df.order = 1 elif method == GEAR2: from . import gear as df df.order = 2 elif method == GEAR3: from . import gear as df df.order = 3 elif method == GEAR4: from . import gear as df df.order = 4 elif method == GEAR5: from . import gear as df df.order = 5 elif method == GEAR6: from . import gear as df df.order = 6 else: df = import_custom_df_module(method, print_out=(outfile != "stdout")) # df is none if module is not found if df is None: sys.exit(23) if not df.has_ff() and use_step_control: printing.print_warning("The chosen DF does not support step control. Turning off the feature.") use_step_control = False #use_aposteriori_step_control = False printing.print_info_line(("done.", 5), verbose) # setup the data buffer # if you use the step control, the buffer has to be one point longer. # That's because the excess point is used by a FF in the df module to predict the next value. printing.print_info_line(("Setting up the buffer... ", 5), verbose, print_nl=False) ((max_x, max_dx), (pmax_x, pmax_dx)) = df.get_required_values() if max_x is None and max_dx is None: printing.print_general_error("df doesn't need any value?") sys.exit(1) if use_step_control: buffer_len = 0 for mx in (max_x, max_dx, pmax_x, pmax_dx): if mx is not None: buffer_len = max(buffer_len, mx) buffer_len += 1 thebuffer = dfbuffer(length=buffer_len, width=3) else: thebuffer = dfbuffer(length=max(max_x, max_dx) + 1, width=3) thebuffer.add((tstart, x0, None)) #setup the first values printing.print_info_line(("done.", 5), verbose) #FIXME #setup the output buffer if return_req_dict: output_buffer = dfbuffer(length=return_req_dict["points"], width=1) output_buffer.add((x0,)) else: output_buffer = None # import implicit_euler to be used in the first iterations # this is because we don't have any dx when we start, nor any past point value if (max_x is not None and max_x > 0) or max_dx is not None: from . import implicit_euler first_iterations_number = max_x if max_x is not None else 1 first_iterations_number = max( first_iterations_number, max_dx+1) \ if max_dx is not None else first_iterations_number else: first_iterations_number = 0 printing.print_info_line(("MNA (reduced):", 5), verbose) printing.print_info_line((mna, 5), verbose) printing.print_info_line(("D (reduced):", 5), verbose) printing.print_info_line((D, 5), verbose) # setup the initial values to start the iteration: x = None time = tstart nv = circ.get_nodes_number() Gmin_matrix = dc_analysis.build_gmin_matrix(circ, options.gmin, mna.shape[0], verbose) # lo step viene generato automaticamente, ma non superare mai quello fornito. if use_step_control: #tstep = min((tstop-tstart)/9999.0, HMAX, 100.0 * options.hmin) tstep = min((tstop-tstart)/9999.0, HMAX) printing.print_info_line(("Initial step: %g"% (tstep,), 5), verbose) if max_dx is None: max_dx_plus_1 = None else: max_dx_plus_1 = max_dx +1 if pmax_dx is None: pmax_dx_plus_1 = None else: pmax_dx_plus_1 = pmax_dx +1 # setup error vectors aerror = np.zeros((x0.shape[0], 1)) aerror[:nv-1, 0] = options.vea aerror[nv-1:, 0] = options.vea rerror = np.zeros((x0.shape[0], 1)) rerror[:nv-1, 0] = options.ver rerror[nv-1:, 0] = options.ier iter_n = 0 # contatore d'iterazione # when to start predicting the next point start_pred_iter = max(*[i for i in (0, pmax_x, pmax_dx_plus_1) if i is not None]) lte = None sol = results.tran_solution(circ, tstart, tstop, op=x0, method=method, outfile=outfile) printing.print_info_line(("Solving... ", 3), verbose, print_nl=False) tick = ticker.ticker(increments_for_step=1) tick.display(verbose > 1) while time < tstop: if iter_n < first_iterations_number: x_coeff, const, x_lte_coeff, prediction, pred_lte_coeff = \ implicit_euler.get_df((thebuffer.get_df_vector()[0],), tstep, \ predict=(use_step_control and iter_n >= start_pred_iter)) else: x_coeff, const, x_lte_coeff, prediction, pred_lte_coeff = \ df.get_df(thebuffer.get_df_vector(), tstep, predict=(use_step_control and iter_n >= start_pred_iter) ) if options.transient_prediction_as_x0 and use_step_control and prediction is not None: x0 = prediction elif x is not None: x0 = x x1, error, solved, n_iter = dc_analysis.dc_solve( mna=(mna + np.multiply(x_coeff, D)), Ndc=N, Ntran=np.dot(D, const), circ=circ, Gmin=Gmin_matrix, x0=x0, time=(time + tstep), locked_nodes=locked_nodes, MAXIT=options.transient_max_nr_iter, verbose=0 ) if solved: old_step = tstep #we will modify it, if we're using step control otherwise it's the same # step control (yeah) if use_step_control: if x_lte_coeff is not None and pred_lte_coeff is not None and prediction is not None: # this is the Local Truncation Error :) lte = abs((x_lte_coeff / (pred_lte_coeff - x_lte_coeff)) * (prediction - x1)) # it should NEVER happen that new_step > 2*tstep, for stability new_step_coeff = 2 for index in range(x.shape[0]): if lte[index, 0] != 0: new_value = ((aerror[index, 0] + rerror[index, 0]*abs(x[index, 0])) / lte[index, 0]) \ ** (1.0 / (df.order+1)) if new_value < new_step_coeff: new_step_coeff = new_value #print new_value new_step = tstep * new_step_coeff if (options.transient_use_aposteriori_step_control and new_step_coeff < options.transient_aposteriori_step_threshold): #don't recalculate a x for a small change tstep = check_step(new_step, time, tstop, HMAX) #print "Apost. (reducing) step = "+str(tstep) continue tstep = check_step(new_step, time, tstop, HMAX) # used in the next iteration #print "Apriori tstep = "+str(tstep) else: #print "LTE not calculated." lte = None if print_step_and_lte and lte is not None: #if you wish to look at the step. We print just a lte flte.write(str(time)+"\t"+str(old_step)+"\t"+str(lte.max())+"\n") # if we get here, either aposteriori_step_control is # disabled, or it's enabled and the error is small # enough. Anyway, the result is GOOD, STORE IT. time = time + old_step x = x1 iter_n = iter_n + 1 sol.add_line(time, x) dxdt = np.multiply(x_coeff, x) + const thebuffer.add((time, x, dxdt)) if output_buffer is not None: output_buffer.add((x, )) tick.step() else: # If we get here, Newton failed to converge. We need to reduce the step... if use_step_control: tstep = tstep/5.0 tstep = check_step(tstep, time, tstop, HMAX) printing.print_info_line(("At %g s reducing step: %g s (convergence failed)" % (time, tstep), 5), verbose) else: #we can't reduce the step printing.print_general_error("Can't converge with step "+str(tstep)+".") printing.print_general_error("Try setting --t-max-nr to a higher value or set step to a lower one.") solved = False break if options.transient_max_time_iter and iter_n == options.transient_max_time_iter: printing.print_general_error("MAX_TIME_ITER exceeded ("+str(options.transient_max_time_iter)+"), iteration halted.") solved = False break if print_step_and_lte: flte.close() tick.hide(verbose > 1) if solved: printing.print_info_line(("done.", 3), verbose) printing.print_info_line(("Average time step: %g" % ((tstop - tstart)/iter_n,), 3), verbose) if output_buffer: ret_value = output_buffer.get_as_matrix() else: ret_value = sol else: print("failed.") ret_value = None return ret_value
[docs]def check_step(tstep, time, tstop, HMAX): """Checks the step for several common issues and corrects them. The following problems are checked: - the step must be shorter than ``HMAX``. In the context of a transient analysis, that usually is the time step provided by the user, - the step must be equal or shorter than the simulation time left (ie stop time minus current time), - the step must be longer than ``options.hmin``, the minimum allowable time step. If the step goes below this value, convergence problems due to machine precision will occur. Typically when this happens, we halt the simulation. **Parameters:** tstep : float The time step, in second, that needs to be checked. time : float The current simulation time. tstop : float The time at which the simulation ends. HMAX : float The maximum allowable time step. **Returns:** tstep : float The step provided if it passes the tests, a *shortened* step otherwise. :raises ValueError: When the step is shorter than ``option.hmin``. """ if tstep > HMAX: tstep = HMAX if tstop - time < tstep: tstep = tstop - time elif tstep < options.hmin: printing.print_general_error("Step size too small: "+str(tstep)) raise ValueError("Step size too small") return tstep
[docs]def generate_D(circ, shape): """Generates the D matrix For every time t, the D matrix is used (elsewhere) to solve the following system: .. math:: D dx/dt + MNA x + N + T(x) = 0 It's easy to set up the KCL law for the voltage unknowns, capacitors introduce stamps just like resistors do in the MNA and we know that row 1 refers to node 1, row 2 refers to node 2, and so on Inductors generate, together with voltage sources, ccvs, vcvs, a additional line in the MNA matrix, and hence in D too. The current flowing through the device gets added to the x vector. In the case of an inductors, we have: .. math:: V(n_1) - V(n_2) - V_L = 0 Where: .. math:: V_L = L dI/dt That's 0 (zero) in DC analysis, but not in transient analysis, where it needs to be differentiated. To understand on which line does the inductor's L*dI/dt go, we use the order of the elements in `circuit`: first are all voltage lines, then the current ones in the same order of the elements that introduce them. Therefore, we need to access the circuit (`circ`). **Parameters:** circ : circuit instance The circuit instance for which the :math:`D` matrix is computed. shape : tuple of ints The shape of the *reduced* :math:`MNA` matrix, D will be of the same shape. **Returns:** D : ndarray The *unreduced* D matrix. """ D = np.zeros((shape[0]+1, shape[1]+1)) nv = circ.get_nodes_number()# - 1 i_eq = 0 #each time we find a vsource or vcvs or ccvs, we'll add one to this. for elem in circ: if circuit.is_elem_voltage_defined(elem) and not isinstance(elem, components.Inductor): i_eq = i_eq + 1 elif isinstance(elem, components.Capacitor): n1 = elem.n1 n2 = elem.n2 D[n1, n1] = D[n1, n1] + elem.value D[n1, n2] = D[n1, n2] - elem.value D[n2, n2] = D[n2, n2] + elem.value D[n2, n1] = D[n2, n1] - elem.value elif isinstance(elem, components.Inductor): D[ nv + i_eq, nv + i_eq ] = -1 * elem.value # Mutual inductors (coupled inductors) # need to add a -M dI/dt where I is the current in the OTHER inductor. if len(elem.coupling_devices): for cd in elem.coupling_devices: # get id+descr 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. D[ nv + i_eq, nv + other_index ] += -1 * cd.M # carry on as usual i_eq = i_eq + 1 if options.cmin > 0: cmin_mat = np.eye(shape[0]+1-i_eq) cmin_mat[0, 1:] = 1 cmin_mat[1:, 0] = 1 cmin_mat[0, 0] = cmin_mat.shape[0]-1 if i_eq: D[:-i_eq, :-i_eq] += options.cmin*cmin_mat else: D += options.cmin*cmin_mat return D
[docs]class dfbuffer: """This is a LIFO buffer with a method to read it all without deleting the elements. Newer entries are added on top of the buffer. It checks the size of the added elements, to be sure they are of the same size. **Parameters:** length : int The length of the buffer. Samples are added at index ``0``, shifting all the previous samples back to higher indices. Samples at an index equal to ``length`` (or higher) are discarded without notice. width : int The width of the buffer, every time :func:`add` is called, it must be to add a tuple of the same length as this parameter. """ _the_real_buffer = None _length = 0 _width = 0 def __init__(self, length, width): self._the_real_buffer = [] self._length = length self._width = width
[docs] def add(self, atuple): """Add a new data point to the buffer. **Parameters:** atuple : tuple of floats The data point to be added. Notice that the length of the tuple must agree with the width of the buffer. :raises ValueError: if the provided tuple and the buffer width do not match. """ if not len(atuple) == self._width: raise ValueError("Attempted to add a element of wrong size to the" + "LIFO buffer.") self._the_real_buffer.insert(0, atuple) if len(self._the_real_buffer) > self._length: self._the_real_buffer = self._the_real_buffer[:self._length]
[docs] def get_df_vector(self): """Read out the contents of the buffer, without any modification This method, in the context of a transient analysis, returns a vector suitable for a differentiation formula. **Returns:** vec : list of tuples a list of tuples, each tuple being composed of ``width`` floats. In the context of a transient analysis, the list (or vector) conforms to the specification of the differentiation formulae. That is, the simulator stores in the buffer a list similar to:: [[time(n), x(n), dx(n)], [time(n-1), x(n-1), dx(n-1)], ...] """ return self._the_real_buffer
[docs] def isready(self): """This shouldn't be used to determine if the buffer has enough points to use the df _if_ you use the step control. In that case, it holds even the points required for the FF. """ if len(self._the_real_buffer) == self._length: return True else: return False
[docs] def get_as_matrix(self): for vindex in range(self._width): for index in range(len(self._the_real_buffer)): if index == 0: single_matrix = self._the_real_buffer[index][vindex] else: single_matrix = np.concatenate((self._the_real_buffer[index][vindex], single_matrix), axis=0) if vindex == 0: complete_matrix = single_matrix else: complete_matrix = np.concatenate((complete_matrix, single_matrix), axis=1) return complete_matrix
[docs]def import_custom_df_module(method, print_out): """Imports a module that implements differentiation formula through imp.load_module Parameters: method: a string, the name of the df method module print_out: print to stdout some verbose messages Returns: The df module or None if the module is not found. """ try: df = imp.load_module(imp.find_module(method.lower())) if print_out: print("Custom df module "+method.lower()+" loaded.") except: printing.print_general_error("Unrecognized method: "+method.lower()+".") df = None return df