# -*- coding: iso-8859-1 -*-
# ac.py
# AC analysis module
# Copyright 2010 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 contains the methods required to perform an AC analysis.
.. note::
Typically, the user does not need to call the functions in this module
directly, instead, we recommend defining an AC analysis object through the
convenience method :func:`ahkab.ahkab.new_ac` and then running it calling
:func:`ahkab.ahkab.run`.
Overview of AC simulations
--------------------------
The AC simulation problem
'''''''''''''''''''''''''
Our AC analysis problem can be written as:
.. math::
MNA\\ x + AC(\\omega )\\ x + J x + N_{ac}(\\omega) = 0
We need:
1. the Modified Nodal Analysis matrix :math:`MNA`,
2. the :math:`AC` matrix, holding the frequency dependent parts,
3. :math:`J`, the Jacobian matrix from the linearized non-linear elements,
4. :math:`N_{ac}`, the AC sources contribution.
An Operating Point (OP) has to be computed first if there is any non-linear
device in the circuit to perform the linearization.
When all the matrices are available, it is possible to solve the system
for the frequency values specified by the user, providing the resulting
matrix is not singular (and possibly well conditioned).
Building the AC matrix
''''''''''''''''''''''
It's easy to set up the voltage lines, since line 2 refers to
node 2, etc...
A capacitor between two example nodes ``n1`` and ``n2`` introduces the
following elements:
.. math::
\\mathrm{(KCL\\ node\\ n1)}\\qquad +j\\omega C\\ V(n1) - j\\omega C V(n2) + ... = ...
\\mathrm{(KCL\\ node\\ n2)}\\qquad -j\\omega C\\ V(n1) + j\\omega C V(n2) + ... = ...
Inductors generate, together with voltage sources, Current-Controlled Voltage
sources (CCVS), Voltage-Controlled Voltage Sources (VCVS), an
additional line in the :math:`MNA` matrix, and hence in :math:`AC` too.
In fact, the current flowing through the device gets added to the unknowns
vector, :math:`x`.
For example, in the case of an inductors, we have:
.. math::
\\mathrm{(KVL\\ over\\ n1\\ and\\ n2)}\\qquad V(n1) - V(n2) - j\\omega L\\ I(\\mathrm{inductor}) = 0
To understand on which line is the KVL line for an inductor, we use the
*order* of the elements in :mod:`ahkab.circuit`:
* first are assembled all the voltage rows,
* then the current rows, in the same order in which the elements that introduce
them are found in :class:`ahkab.circuit.Circuit`.
Solving
'''''''
For each angular frequency :math:`\\omega`, the simulator solves the matrix
equation described.
Since the equation is linear, solving is performed with a single matrix
inversion and multiplication for each step.
Module reference
----------------
"""
from __future__ import (unicode_literals, absolute_import,
division, print_function)
import numpy as np
from . import (circuit, dc_analysis, components, options, printing, results,
utilities)
specs = {'ac': {'tokens': ({
'label': 'type',
'pos': 0,
'type': str,
'needed': False,
'dest': 'sweep_type',
'default': options.ac_log_step
},
{
'label': 'nsteps',
'pos': 1,
'type': float,
'needed': True,
'dest': 'points',
'default': None
},
{
'label': 'start',
'pos': 2,
'type': float,
'needed': True,
'dest': 'start',
'default': None
},
{
'label': 'stop',
'pos': 3,
'type': float,
'needed': True,
'dest': 'stop',
'default': None
})
}
}
[docs]def ac_analysis(circ, start, points, stop, sweep_type=None,
x0=None, mna=None, AC=None, Nac=None, J=None,
outfile="stdout", verbose=3):
"""Performs an AC analysis.
**Parameters:**
circ : Circuit instance
The circuit to be simulated.
start : float
The start frequency for the AC analysis, in Hz.
points : float,
The number of points to be used to discretize the
``[start, stop]`` interval.
stop : float
The stop frequency, in Hz.
sweep_type : string, optional
Either ``options.ac_log_step`` (ie ``'LOG'``) or ``options.ac_lin_step``
(ie ``'LIN'``), defaults to ``options.ac_log_step``, resulting in a
logarithmic sweep.
x0 : OP results instance, optional
The linearization point. If not set, it will be computed
running an OP analysis.
mna, AC, Nax, J : ndarrays, optional
The matrices to perform the analysis. They will be computed
if not supplied.
outfile : string, optional
The name of the file where the results will be written.
The suffix ``'.ac'`` is automatically added at the end of the string to
prevent different analyses from overwriting each-other's results. Set to
``'stdout'`` to write to the standard output. If unset, or set to
``None``, defaults to the standard output.
verbose : int, optional
The verbosity level, from 0 (silent) to 6 (debug).
**Returns:**
ACresult : AC solution
The AC analysis results.
:raises ValueError: if the parameters are out of their valid range.
:raises RuntimeError: if the circuit is non-linear and can't be linearized.
"""
if outfile == 'stdout':
verbose = 0
# check step/start/stop parameters
if start == 0:
raise ValueError("AC analysis has start frequency = 0")
if start > stop:
raise ValueError("AC analysis has start > stop")
if points < 2 and not start == stop:
raise ValueError("AC analysis has number of points < 2 & start != stop")
if sweep_type.upper() == options.ac_log_step or sweep_type is None:
omega_iter = utilities.log_axis_iterator(2*np.pi*start, 2*np.pi*stop, points)
elif sweep_type.upper() == options.ac_lin_step:
omega_iter = utilities.lin_axis_iterator(2*np.pi*start, 2*np.pi*stop, points)
else:
raise ValueError("Unknown sweep type %s" % sweep_type)
tmpstr = "Vea =", options.vea, "Ver =", options.ver, "Iea =", options.iea, "Ier =", \
options.ier, "max_ac_nr_iter =", options.ac_max_nr_iter
printing.print_info_line((tmpstr, 5), verbose)
del tmpstr
printing.print_info_line(("Starting AC analysis: ", 1), verbose)
tmpstr = "w: start = %g Hz, stop = %g Hz, %d points" % (start, stop, points)
printing.print_info_line((tmpstr, 3), verbose)
del tmpstr
# It's a good idea to call AC with prebuilt MNA matrix if the circuit is
# big
if mna is None:
mna, N = dc_analysis.generate_mna_and_N(circ, verbose=verbose)
del N
mna = utilities.remove_row_and_col(mna)
if Nac is None:
Nac = _generate_Nac(circ)
Nac = utilities.remove_row(Nac, rrow=0)
if AC is None:
AC = _generate_AC(circ, [mna.shape[0], mna.shape[0]])
AC = utilities.remove_row_and_col(AC)
if circ.is_nonlinear():
if J is not None:
pass
# we used the supplied linearization matrix
else:
if x0 is None:
printing.print_info_line(("Starting OP analysis to get a " +
"linearization point...", 3), verbose,
print_nl=False)
# silent OP
x0 = dc_analysis.op_analysis(circ, verbose=0)
if x0 is None: # still! Then op_analysis has failed!
printing.print_info_line(("failed.", 3), verbose)
raise RuntimeError("OP analysis failed, no " +
"linearization point available.")
else:
printing.print_info_line(("done.", 3), verbose)
printing.print_info_line(
("Linearization point (xop):", 5), verbose)
if verbose > 4:
x0.print_short()
printing.print_info_line(("Linearizing the circuit...", 5), verbose,
print_nl=False)
J = _generate_J(xop=x0.asarray(), circ=circ,
reduced_mna_size=mna.shape[0])
printing.print_info_line((" done.", 5), verbose)
# we have J, continue
else: # not circ.is_nonlinear()
# no J matrix is required.
J = 0
printing.print_info_line(("MNA (reduced):", 5), verbose)
printing.print_info_line((mna, 5), verbose)
printing.print_info_line(("AC (reduced):", 5), verbose)
printing.print_info_line((AC, 5), verbose)
printing.print_info_line(("J (reduced):", 5), verbose)
printing.print_info_line((J, 5), verbose)
printing.print_info_line(("Nac (reduced):", 5), verbose)
printing.print_info_line((Nac, 5), verbose)
sol = results.ac_solution(circ, start=start, stop=stop, points=points,
stype=sweep_type, op=x0, outfile=outfile)
# setup the initial values to start the iteration:
j = np.complex('j')
Gmin_matrix = dc_analysis.build_gmin_matrix(
circ, options.gmin, mna.shape[0], verbose)
iter_n = 0 # contatore d'iterazione
printing.print_info_line(("Solving... ", 3), verbose, print_nl=False)
x = x0
for omega in omega_iter:
x, _, solved, _ = dc_analysis.dc_solve(
mna=(mna + np.multiply(j * omega, AC) + J),
Ndc = Nac,
Ntran = 0,
circ = circuit.Circuit(
title="Dummy circuit for AC", filename=None),
Gmin = Gmin_matrix,
x0 = x,
time = None,
locked_nodes = None,
MAXIT = options.ac_max_nr_iter,
skip_Tt = True,
verbose = 0)
if solved:
iter_n = iter_n + 1
# hooray!
sol.add_line(omega/np.pi/2, x)
else:
break
if solved:
printing.print_info_line(("done.", 1), verbose)
ret_value = sol
else:
printing.print_info_line(("failed.", 1), verbose)
ret_value = None
return ret_value
def _generate_AC(circ, shape):
"""Generates the AC coefficients matrix.
**Parameters:**
circ : Circuit instance
The circuit instance for which the :math:`AC` matrix will be generated.
shape : int
The reduced MNA size.
**Returns:**
AC : ndarray
The *unreduced* (full size) :math:`AC` matrix.
"""
AC = 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
AC[n1, n1] = AC[n1, n1] + elem.value
AC[n1, n2] = AC[n1, n2] - elem.value
AC[n2, n2] = AC[n2, n2] + elem.value
AC[n2, n1] = AC[n2, n1] - elem.value
elif isinstance(elem, components.Inductor):
AC[nv + i_eq, nv + i_eq] = -1 * elem.value
if len(elem.coupling_devices):
for cd in elem.coupling_devices:
# 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.
AC[nv + i_eq, nv + other_index] += -1 * cd.M
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:
AC[:-i_eq, :-i_eq] += options.cmin * cmin_mat
else:
AC += options.cmin * cmin_mat
return AC
def _generate_Nac(circ):
"""Generate the vector holding the contribution of AC sources.
**Parameters:**
circ : Circuit instance
The circuit instance for which the :math:`N_{ac}` matrix will be
generated.
**Returns:**
Nac : ndarray
The constant term :math:`N_{ac}`.
"""
n_of_nodes = circ.get_nodes_number()
Nac = np.zeros((n_of_nodes, 1), dtype=complex)
j = np.complex('j')
# process `ISource`s
for elem in circ:
if isinstance(elem, components.sources.ISource) and elem.abs_ac is not None:
# convenzione normale!
Nac[elem.n1, 0] = Nac[elem.n1, 0] + \
elem.abs_ac * np.exp(j * elem.arg_ac)
Nac[elem.n2, 0] = Nac[elem.n2, 0] - \
elem.abs_ac * np.exp(j * elem.arg_ac)
# process vsources
# 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 = Nac.shape[0]
Nac = utilities.expand_matrix(Nac, add_a_row=True, add_a_col=False)
if isinstance(elem, components.sources.VSource) and elem.abs_ac is not None:
Nac[index, 0] = -1.0 * elem.abs_ac * np.exp(j * elem.arg_ac)
return Nac
def _generate_J(xop, circ, reduced_mna_size):
"""Build the linearized matrix :math:`J`
**Parameters:**
xop : ndarray
The linearization point, as a ``numpy`` ndarray.
circ : Circuit instance
The circuit for which :math:`J` is to be generated.
reduced_mna_size : int
The size of the (square) Modified Nodal Analysis Matrix, after
reduction.
**Returns:**
J : ndarray of size ``(reduced_mna_size, reduced_mna_size)``
The reduced Jacobian :math:`J`.
"""
# setup J
J = np.zeros((reduced_mna_size, reduced_mna_size))
Tlin = np.zeros((reduced_mna_size, 1))
for elem in circ:
if elem.is_nonlinear:
dc_analysis._update_J_and_Tx(J, Tlin, xop, elem, time=None)
# del Tlin # not needed! **DC**!
return J