Commit e774a0d0 authored by Sean Leavey's avatar Sean Leavey

Move solving logic from `circuit` to `analysis`, and split out matrix library

This commit separates the circuit definition (resistors, op-amps, etc.) from the
solver and matrix libraries. Now, in theory, the circuit definition can be used
not just for small signal AC analysis (as was previously available), but also
for any other linear circuit analysis. Furthermore, the matrix library has been
split into its own module - `solve` - to allow for other libraries to be used
in the future. The matrix library preference is controlled via the configuration
file.
parent 61c021a3
This diff is collapsed.
import abc
import copy
import statistics
from ..components import (Component, Resistor, Capacitor, Inductor, OpAmp,
Input, Node, ComponentNoise, NodeNoise)
class BaseAnalysis(object, metaclass=abc.ABCMeta):
"""Base class for circuit analysis"""
def __init__(self, circuit):
self.circuit = circuit
def component_index(self, component):
"""Get component serial number.
Parameters
----------
component : :class:`~.Component`
component
Returns
-------
:class:`int`
component serial number
Raises
------
ValueError
if component not found
"""
return self.circuit.components.index(component)
def node_index(self, node):
"""Get node serial number.
This does not include the ground node, so the first non-ground node
has serial number 0.
Parameters
----------
node : :class:`~.Node`
node
Returns
-------
:class:`int`
node serial number
Raises
------
ValueError
if ground node is specified or specified node is not found
"""
if node == Node("gnd"):
raise ValueError("ground node does not have an index")
return list(self.circuit.non_gnd_nodes).index(node)
@property
def elements(self):
"""Matrix elements.
Returns a sequence of elements - either components or nodes - in the
order in which they appear in the matrix
Yields
------
:class:`~.components.Component`, :class:`~.components.Node`
matrix elements
"""
yield from self.circuit.components
yield from self.circuit.non_gnd_nodes
@property
def element_names(self):
"""Names of elements (components and nodes) within the circuit.
Yields
------
:class:`str`
matrix element names
"""
return [element.name for element in self.elements]
@property
def mean_resistance(self):
"""Average circuit resistance"""
return statistics.mean([resistor.resistance for resistor in self.circuit.resistors])
\ No newline at end of file
......@@ -12,6 +12,9 @@ size_y = 6
[matplotlib]
legend.fontsize = 9
[algebra]
solver = scipy-default
[data]
tf_abs_tol = 1e-4
tf_rel_tol = 1e-4
......
This diff is collapsed.
This diff is collapsed.
......@@ -22,6 +22,7 @@ from .circuit import Circuit
from .components import (Component, Resistor, Capacitor, Inductor, OpAmp, Input,
Node, CurrentNoise, VoltageNoise, JohnsonNoise)
from .solution import Solution
from .analysis.ac import SmallSignalAcAnalysis
LOGGER = logging.getLogger("liso")
......@@ -114,12 +115,12 @@ class BaseParser(object, metaclass=abc.ABCMeta):
self._add_circuit_input()
if self.output_type is self.TYPE_NOISE:
return self.circuit.calculate_noise(
return SmallSignalAcAnalysis(circuit=self.circuit).calculate_noise(
frequencies=self.frequencies,
noise_node=self.noise_node,
*args, **kwargs)
elif self.output_type is self.TYPE_TF:
return self.circuit.calculate_tfs(
return SmallSignalAcAnalysis(circuit=self.circuit).calculate_tfs(
frequencies=self.frequencies,
output_components=self.output_components,
output_nodes=self.output_nodes,
......@@ -343,7 +344,7 @@ class BaseParser(object, metaclass=abc.ABCMeta):
# functions
if self.noise_node is not None:
# we're calculating noise
input_type = Input.TYPE_NOISE
input_type = "noise"
# set input impedance
impedance = self.input_impedance
......@@ -422,11 +423,10 @@ class InputParser(BaseParser):
:type lines: Sequence[str]
"""
# open file
with open(self.filepath, "r") as obj:
for tokens in [self.tokenise(line) for line in lines
if not line.startswith("#")]:
self._parse_tokens(tokens)
# parse tokens
for tokens in [self.tokenise(line) for line in lines
if not line.startswith("#")]:
self._parse_tokens(tokens)
# check we found anything
self.validate()
......@@ -507,7 +507,7 @@ class InputParser(BaseParser):
self.input_node_p = Node(options[0])
if input_type == "uinput":
self.input_type = Input.TYPE_VOLTAGE
self.input_type = "voltage"
if len(options) > 2:
# floating input
self.input_node_n = Node(options[1])
......@@ -526,7 +526,7 @@ class InputParser(BaseParser):
LOGGER.info("adding voltage input node %s with default impedance of 50Ω",
self.input_node_p)
elif input_type == "iinput":
self.input_type = Input.TYPE_CURRENT
self.input_type = "current"
self.input_node_n = None
if len(options) == 2:
self.input_impedance, _ = SIFormatter.parse(options[1])
......@@ -892,7 +892,7 @@ class OutputParser(BaseParser):
if match_fixed_current:
# fixed current input
self.input_type = Input.TYPE_CURRENT
self.input_type = "current"
self.input_node_p = Node(match_fixed_current.group(1))
self.input_impedance, _ = SIFormatter.parse(match_fixed_current.group(2))
......@@ -902,7 +902,7 @@ class OutputParser(BaseParser):
return
elif match_fixed_voltage:
# fixed voltage input
self.input_type = Input.TYPE_VOLTAGE
self.input_type = "voltage"
self.input_node_p = Node(match_fixed_voltage.group(1))
self.input_impedance, _ = SIFormatter.parse(match_fixed_voltage.group(2))
......@@ -912,7 +912,7 @@ class OutputParser(BaseParser):
return
elif match_floating_voltage:
# floating input
self.input_type = Input.TYPE_VOLTAGE
self.input_type = "voltage"
self.input_node_p = Node(match_floating_voltage.group(1))
self.input_node_m = Node(match_floating_voltage.group(2))
self.input_impedance, _ = SIFormatter.parse(match_floating_voltage.group(3))
......@@ -999,11 +999,11 @@ class OutputParser(BaseParser):
phase_scale=phase_scale)
# create appropriate transfer function depending on input type
if self.input_type is Input.TYPE_VOLTAGE:
if self.input_type is "voltage":
function = VoltageVoltageTF(series=series,
source=self.input_node_p,
sink=sink)
elif self.input_type is Input.TYPE_CURRENT:
elif self.input_type is "current":
function = CurrentVoltageTF(series=series,
source=self.circuit.get_component("input"),
sink=sink)
......@@ -1066,11 +1066,11 @@ class OutputParser(BaseParser):
phase_scale=phase_scale)
# create appropriate transfer function depending on input type
if self.input_type is Input.TYPE_VOLTAGE:
if self.input_type is "voltage":
function = VoltageCurrentTF(series=series,
source=self.input_node_p,
sink=sink)
elif self.input_type is Input.TYPE_CURRENT:
elif self.input_type is "current":
function = CurrentCurrentTF(series=series,
source=self.circuit.get_component("input"),
sink=sink)
......
from ..config import CircuitConfig
from .scipy import ScipySolver
CONF = CircuitConfig()
# available solver classes
solver_classes = [ScipySolver]
# dict of solver names and types
available_solvers = {_class.NAME: _class for _class in solver_classes}
# get solver from preferences
solver_name = CONF["algebra"]["solver"].lower()
if solver_name not in available_solvers:
raise ValueError("Invalid solver \"%s\" specified in configuration. Choose from %s."
% (solver_name, ", ".join(available_solvers)))
# get solver type
Solver = available_solvers[solver_name]
\ No newline at end of file
import abc
class BaseSolver(object, metaclass=abc.ABCMeta):
"""Base class for matrix solvers"""
# solver name
NAME = "base"
@abc.abstractmethod
def full(self, dimensions):
"""Create new complex-valued full matrix
Parameters
----------
dimensions : :class:`tuple`
matrix shape
"""
raise NotImplementedError
@abc.abstractmethod
def sparse(self, dimensions):
"""Create new complex-valued sparse matrix
Parameters
----------
dimensions : :class:`tuple`
matrix shape
"""
raise NotImplementedError
@abc.abstractmethod
def solve(self, A, b):
"""Solve linear system"""
raise NotImplementedError
\ No newline at end of file
from scipy.sparse import lil_matrix
from scipy.sparse.linalg import spsolve
import numpy as np
from .base import BaseSolver
class ScipySolver(BaseSolver):
"""Scipy-based matrix solver"""
# solver name
NAME = "scipy-default"
def full(self, dimensions):
"""Create new complex-valued full matrix
Creates a Numpy full matrix.
Parameters
----------
dimensions : :class:`tuple`
matrix shape
Returns
-------
:class:`~np.ndmatrix`
full matrix
"""
return np.zeros(dimensions, dtype="complex64")
def sparse(self, dimensions):
"""Create new complex-valued sparse matrix
Creates a SciPy sparse matrix.
Parameters
----------
dimensions : :class:`tuple`
matrix shape
Returns
-------
:class:`~lil_matrix`
sparse matrix
"""
# complex64 gives real and imaginary parts each represented as 32-bit floats
# with 8 bits exponent and 23 bits mantissa, giving between 6 and 7 digits
# of precision; good enough for most purposes
return lil_matrix(dimensions, dtype="complex128")
def solve(self, A, b):
"""Solve linear system
Parameters
----------
A : :class:`~np.ndarray`, :class:`~scipy.sparse.spmatrix`
square matrix
B : :class:`~np.ndarray`, :class:`~scipy.sparse.spmatrix`
matrix or vector representing right hand side of matrix equation
Returns
-------
solution : :class:`~np.ndarray`, :class:`~scipy.sparse.spmatrix`
x in the equation Ax = b
"""
# permute specification chosen to minimise error with LISO
return spsolve(A, b, permc_spec="MMD_AT_PLUS_A")
......@@ -12,6 +12,7 @@ import numpy as np
from circuit import logging_on
logging_on()
from circuit.circuit import Circuit
from circuit.analysis.ac import SmallSignalAcAnalysis
# frequency vector
frequencies = np.logspace(0, 6, 1000)
......@@ -29,9 +30,10 @@ circuit.add_library_opamp(name="o1", model="LT1124", node1="gnd", node2="nm",
node3="nout")
# solve circuit
solution = circuit.calculate_tfs(frequencies, output_components="all",
output_nodes="all", print_equations=True,
print_matrix=True)
analysis = SmallSignalAcAnalysis(circuit=circuit)
solution = analysis.calculate_tfs(frequencies, output_components="all",
output_nodes="all", print_equations=True,
print_matrix=True)
# plot
solution.plot()
......@@ -12,6 +12,7 @@ import numpy as np
from circuit import logging_on
logging_on()
from circuit.circuit import Circuit
from circuit.analysis.ac import SmallSignalAcAnalysis
# frequency vector
frequencies = np.logspace(0, 6, 1000)
......@@ -29,9 +30,10 @@ circuit.add_library_opamp(name="o1", model="LT1124", node1="gnd", node2="nm",
node3="nout")
# solve circuit
solution = circuit.calculate_tfs(frequencies, output_components="all",
output_nodes="all", print_equations=True,
print_matrix=True)
analysis = SmallSignalAcAnalysis(circuit=circuit)
solution = analysis.calculate_tfs(frequencies, output_components="all",
output_nodes="all", print_equations=True,
print_matrix=True)
# plot
solution.plot()
......@@ -11,6 +11,7 @@ import numpy as np
from circuit import logging_on
logging_on()
from circuit.circuit import Circuit
from circuit.analysis.ac import SmallSignalAcAnalysis
# frequency vector
frequencies = np.logspace(0, 6, 1000)
......@@ -28,8 +29,9 @@ circuit.add_library_opamp(name="o1", model="LT1124", node1="gnd", node2="nm",
node3="nout")
# solve circuit
solution = circuit.calculate_noise(frequencies, noise_node="nout",
print_equations=True, print_matrix=True)
analysis = SmallSignalAcAnalysis(circuit=circuit)
solution = analysis.calculate_noise(frequencies, noise_node="nout",
print_equations=True, print_matrix=True)
# plot
solution.plot()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment