Commit 385bb4fe authored by Sean Leavey's avatar Sean Leavey

Merge branch 'release-0.5.4'

parents 3bae5f51 fae0119b
Pipeline #38907 failed with stages
in 1 minute and 38 seconds
......@@ -112,6 +112,18 @@ circuit is solved with the constraint that the voltage between ``n1`` and ground
``1``. The solver can adjust all of the other non-zero matrix elements in the left hand side until
this condition is met within some level of tolerance.
Prescaling
~~~~~~~~~~
By default, |Zero| prescales matrices used in the computation of the solution, in order to improve
numerical precision. This behaviour is currently only implemented for signal analyses and not for
noise.
.. warning::
When prescaling is switched on, any functions which output the circuit matrix (such as the
``--no-prescale`` :ref:`CLI flag <cli/liso:Prescaling>`) will not match the underlying values they represent, but instead
those of the prescaled matrix.
Available analyses
~~~~~~~~~~~~~~~~~~
......
......@@ -112,8 +112,9 @@ Prescaling
----------
|Zero| can prescale its circuit matrices in the same way that LISO does, to help improve numerical
precision. By default, this behaviour is switched on, but can be disabled with the
``--no-prescale`` flag. This option is only available when a native simulation is being performed.
precision (see :ref:`Prescaling <analyses/ac/index:Prescaling>` for more details). By default,
this behaviour is switched on, but can be disabled with the ``--no-prescale`` flag. This option is
only available when a native simulation is being performed.
Saving figures
--------------
......
......@@ -6,6 +6,13 @@ LISO input file parsing
Known incompatibilities
-----------------------
Duplicate component and node names
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In LISO, nodes and components may share the same name, and the user is warned
that this may lead to confusion. In |Zero|, nodes and components cannot share
the same name.
Outputs
~~~~~~~
......
.. include:: /defs.txt
LISO output file parsing
========================
Known incompatibilities
-----------------------
Duplicate component and node names
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In LISO, nodes and components may share the same name, and the user is warned
that this may lead to confusion. In |Zero|, nodes and components cannot share
the same name.
Output coordinates
~~~~~~~~~~~~~~~~~~
......
......@@ -41,7 +41,8 @@ setup(
url="https://git.ligo.org/sean-leavey/zero",
packages=find_packages(),
package_data={
"zero": ["zero.conf.dist", "library.conf.dist"]
"zero": ["zero.conf.dist", "zero.conf.dist.default",
"library.conf.dist", "library.conf.dist.default"]
},
entry_points={
"console_scripts": [
......
......@@ -149,7 +149,7 @@ ioutput load ri1 allop
class NoiseOutputTestCase(LisoInputParserTestCase):
"""Noise output command tests"""
def test_invalid_noise_output_node(self):
def test_invalid_noise_output_element(self):
"""Test nonexistent noise output node"""
self.parser.parse("""
r r1 1k n1 n2
......@@ -159,7 +159,7 @@ freq log 1 1k 100
noise n3 all
""")
self.assertRaisesRegex(LisoParserError,
r"noise output node 'n3' is not present in the circuit",
r"noise output element 'n3' is not present in the circuit",
self.parser.solution)
def test_invalid_noisy_node(self):
......
......@@ -41,9 +41,9 @@ class PrescaleTestCase(TestCase):
noise_node = "n3"
analysis_unscaled = AcNoiseAnalysis(circuit=circuit, node=noise_node, frequencies=self.f,
analysis_unscaled = AcNoiseAnalysis(circuit=circuit, element=noise_node, frequencies=self.f,
prescale=True)
analysis_scaled = AcNoiseAnalysis(circuit=circuit, node=noise_node, frequencies=self.f,
analysis_scaled = AcNoiseAnalysis(circuit=circuit, element=noise_node, frequencies=self.f,
prescale=False)
analysis_unscaled.calculate()
......
......@@ -178,7 +178,7 @@ class NoiseOutputNodeTestCase(LisoInputParserTestCase):
def test_noise(self):
self.parser.parse("noise nout n1")
self.assertEqual(self.parser.output_type, "noise")
self.assertEqual(self.parser.noise_output_node, Node("nout"))
self.assertEqual(self.parser.noise_output_element, "nout")
def test_noise_suffices(self):
text = """
......@@ -207,11 +207,11 @@ op op1 op00 n2 n3 n4
def test_cannot_redefine_noise_node(self):
self.parser.parse("noise nout n1")
# try to set noise node again
self.assertRaisesRegex(LisoParserError, r"cannot redefine noise output node \(line 2\)",
self.assertRaisesRegex(LisoParserError, r"cannot redefine noise output element \(line 2\)",
self.parser.parse, "noise nin n1")
def test_must_set_noise_output_node(self):
# sink node defined, but no sources
def test_must_set_noise_output_element(self):
# sink element defined, but no sources
self.assertRaisesRegex(LisoParserError, r"unexpected end of file \(line 1\)",
self.parser.parse, "noise nout")
......
......@@ -13,12 +13,13 @@ class AcNoiseAnalysisTestCase(TestCase):
def setUp(self):
self.circuit = Circuit()
self.default_f = np.logspace(0, 5, 1000)
self.default_node = Node("nin")
self.default_element = Node("nin")
@property
def default_params(self):
"""Default analysis parameters"""
return {"circuit": self.circuit, "frequencies": self.default_f, "node": self.default_node}
return {"circuit": self.circuit, "frequencies": self.default_f,
"element": self.default_element}
def default_analysis(self):
"""Default analysis"""
......@@ -28,7 +29,7 @@ class AcNoiseAnalysisTestCase(TestCase):
"""Test set noise input"""
self.circuit.add_input(input_type="noise", node="nin", impedance=152.6)
analysis = self.default_analysis()
self.assertEqual(analysis.node, self.default_node)
self.assertEqual(analysis.element, self.default_element)
def test_invalid_input(self):
"""Test input type must be 'noise' for noise analysis"""
......
......@@ -120,7 +120,7 @@ class CircuitTestCase(TestCase):
r2 = Resistor(name="r1", value=2e5, node1="n3", node2="n4")
self.circuit.add_component(r1)
self.assertRaisesRegex(ValueError, r"component with name 'r1' already in circuit",
self.assertRaisesRegex(ValueError, r"element with name 'r1' already in circuit",
self.circuit.add_component, r2)
self.reset()
......@@ -130,5 +130,48 @@ class CircuitTestCase(TestCase):
op1 = OpAmp(name="r1", model="OP00", node1="n3", node2="n4", node3="n5")
self.circuit.add_component(r1)
self.assertRaisesRegex(ValueError, r"component with name 'r1' already in circuit",
self.assertRaisesRegex(ValueError, r"element with name 'r1' already in circuit",
self.circuit.add_component, op1)
def test_cannot_add_component_with_same_name_as_node(self):
"""Test component with same name as existing node cannot be added"""
# first component
r1 = Resistor(name="r1", value=1e3, node1="n1", node2="n2")
# second component, with same name as one of first component's nodes
r2 = Resistor(name="n1", value=2e5, node1="n3", node2="r4")
self.circuit.add_component(r1)
self.assertRaisesRegex(ValueError, r"element with name 'n1' already in circuit",
self.circuit.add_component, r2)
self.reset()
# different component with same name
r1 = Resistor(name="r1", value=1e3, node1="n1", node2="n2")
op1 = OpAmp(name="n1", model="OP00", node1="n2", node2="n4", node3="n5")
self.circuit.add_component(r1)
self.assertRaisesRegex(ValueError, r"element with name 'n1' already in circuit",
self.circuit.add_component, op1)
def test_cannot_add_node_with_same_name_as_component(self):
"""Test node with same name as existing component cannot be added"""
# first component
r1 = Resistor(name="r1", value=1e3, node1="n1", node2="n2")
# second component, with node with same name as first component
r2 = Resistor(name="r2", value=2e5, node1="n3", node2="r1")
self.circuit.add_component(r1)
self.assertRaisesRegex(ValueError, r"node 'r1' is the same as existing circuit component",
self.circuit.add_component, r2)
self.reset()
# different component with same name
r1 = Resistor(name="r1", value=1e3, node1="n1", node2="n2")
op1 = OpAmp(name="op1", model="OP00", node1="r1", node2="n4", node3="n5")
self.circuit.add_component(r1)
self.assertRaisesRegex(ValueError, r"node 'r1' is the same as existing circuit component",
self.circuit.add_component, op1)
# Advanced LIGO UIM Coil Driver
# https://dcc.ligo.org/LIGO-D070481/public
# Autogenerated on 2014-12-05 16:08 by ../../../../Utils/LisoFromEdif.py ../../../Altium/QUAD/UIM/D070481-04-K_LISO.EDF
FREQ LOG 0.1 100k 100
C C22 4.7u NetC22_1 NetC22_2
C C100 220p NetC100_1 NetC100_2
C C23 4.7u NetC23_1 NetC23_2
C C21 4.7u NetC21_1 NetC21_2
C C12 0.68u NetC12_1 NetC12_2
R R103 510 NetIC7_6 NetIC7_2
R R102 510 NetIC3_6 NetIC3_2
R R101 510 NetIC8_6 NetIC8_2
R R100 510 NetIC4_6 NetIC4_2
R R105 10 NetC26_2 NetC101_1
R R104 10 NetC12_2 NetC100_1
R R16 10k NetR16_1 NetR8_2
R R17 3.3k NetR17_1 NetIC4_3
R R15 2.2k NetC101_2 NetR10_1
R R10 2.2k NetR10_1 NetC100_2
R R11 3.3k NetR11_1 NetIC3_3
R R18 1M NetC21_2 NetIC2_3
R R19 1M NetC22_2 NetIC3_3
OP IC8 ad8674 NetC23_1 NetIC8_2 NetIC8_6
L LBOSEM 11.9m NetLBOSEM_1 NetLBOSEM_2
OP IC3 ad8674 NetIC3_3 NetIC3_2 NetIC3_6
OP IC2 ad8674 NetIC2_3 NetC100_2 NetC100_1
OP IC7 ad8674 NetC22_1 NetIC7_2 NetIC7_6
OP IC6 ad8674 NetC21_1 NetC101_2 NetC101_1
OP IC4 ad8674 NetIC4_3 NetIC4_2 NetIC4_6
R R4 750 NetLBOSEM_2 NetC12_1
R R5 2k NetLBOSEM_2 NetC12_2
R R6 16k NetIC2_3 NetIC3_6
R R7 16k NetIC3_3 NetIC4_6
C C26 0.68u NetC26_1 NetC26_2
R R3 3.3k NetC12_2 NetC100_2
R R8 16k NetIC4_3 NetR8_2
R R9 3.3k NetR9_1 NetIC2_3
R R25 16k NetC22_1 NetIC8_6
UINPUT NetR8_2
C C101 220p NetC101_1 NetC101_2
R R27 750 NetR23_1 NetC26_1
R R26 16k NetC23_1 GND
R RBOSEM 42.7 NetR23_1 NetLBOSEM_1
R R24 16k NetC21_1 NetIC7_6
R R23 2k NetR23_1 NetC26_2
R R22 10k GND NetR16_1
R R21 1M NetC23_2 NetIC4_3
R R20 3.3k NetC26_2 NetC101_2
R SWLP3 999M NetC21_2 NetR9_1
R SWLP2 999M NetC22_2 NetR11_1
R SWLP1 999M NetC23_2 NetR17_1
NOISE R20 SUM
NOISY ALL
# example circuit from LISO manual
r r1 100 nin nsum
r r3 1.075k no nsum
r r4 42.2 nsum nm
r r6 65 nin gnd
c c2 4.7n nsum gnd
c c5 122p no nm
op op1 op27 gnd nm no
freq log 1 100k 100
uinput nin 50
noise op1 all
c c1 270.88486n nm2 no
c c2 7.516u no3 np1
c ca 1.484u nm2 ni
op n1a OP27 np1 gnd no1
op n2a OP27 gnd nm2 no
op n3a OP27 gnd nm3 no3
r ri2 2.6924361k nm3 no1
r ri1 1k no3 nm3
r r1 40.246121k no1 nm2
r r2 2.0651912k np1 no
r ra 115.16129k nm2 ni
r rb 2.0154407k np1 ni
r rq 22.9111k nm2 no
r load 1k no gnd
r rin 50 nii ni
freq log 100m 50 100
iinput nii 0
noise load allop
c c1 270.88486n nm2 no
c c2 7.516u no3 np1
c ca 1.484u nm2 ni
op n1a OP27 np1 gnd no1
op n2a OP27 gnd nm2 no
op n3a OP27 gnd nm3 no3
r ri2 2.6924361k nm3 no1
r ri1 1k no3 nm3
r r1 40.246121k no1 nm2
r r2 2.0651912k np1 no
r ra 115.16129k nm2 ni
r rb 2.0154407k np1 ni
r rq 22.9111k nm2 no
r load 1k no gnd
r rin 50 nii ni
freq log 100m 50 100
iinput nii 0
noise no allop
c c1 270.88486n nm2 no
c c2 7.516u no3 np1
c ca 1.484u nm2 ni
op n1a OP27 np1 gnd no1
op n2a OP27 gnd nm2 no
op n3a OP27 gnd nm3 no3
r ri2 2.6924361k nm3 no1
r ri1 1k no3 nm3
r r1 40.246121k no1 nm2
r r2 2.0651912k np1 no
r ra 115.16129k nm2 ni
r rb 2.0154407k np1 ni
r rq 22.9111k nm2 no
r load 1k no gnd
r rin 50 nii ni
freq log 100m 50 100
uinput nii 0
noise load allop
# non-inverting whitening filter
c c1 10u gnd n1
r r1 430 n1 nm
r r2 43k nm nout
c c2 47p nm nout
op o1 lt1124 nin nm nout
freq log 1 100k 100
uinput nin 0
noise o1 all
......@@ -23,15 +23,18 @@ class LisoTester(unittest.TestCase):
# test message
message = "Test %s against LISO" % self.fil_path
with self.subTest(msg=message):
self.compare(self._liso_output())
with self.subTest(msg=message + " (without prescaling)"):
self.compare(self._liso_output(), prescale=False)
def compare(self, liso_output):
with self.subTest(msg=message + " (with prescaling)"):
self.compare(self._liso_output(), prescale=True)
def compare(self, liso_output, **native_kwargs):
# get LISO solution
liso_solution = liso_output.solution()
# run native
native_solution = liso_output.solution(force=True)
native_solution = liso_output.solution(force=True, **native_kwargs)
# check if they match (only check defaults as LISO only generates defaults)
self.assertTrue(liso_solution.equivalent_to(native_solution, defaults_only=True))
......
......@@ -9,12 +9,13 @@ from tabulate import tabulate
from . import __version__, PROGRAM, DESCRIPTION, set_log_verbosity
from .liso import LisoInputParser, LisoOutputParser, LisoRunner, LisoParserError
from .datasheet import PartRequest
from .config import ZeroConfig
from .config import ZeroConfig, OpAmpLibrary
from .library import LibraryQueryEngine
from .misc import open_file
CONF = ZeroConfig()
LOGGER = logging.getLogger(__name__)
CONF = ZeroConfig()
LIBRARY = OpAmpLibrary()
# Shared arguments:
......@@ -277,25 +278,12 @@ def library_path():
Note: this path may not exist.
"""
print(CONF.get_user_config_filepath())
print(LIBRARY.get_user_config_filepath())
@library.command("open")
def library_open():
"""Open user op-amp library file for editing.
If the op-amp library doesn't exist, it is created.
"""
path = CONF.get_user_config_filepath()
if not os.path.exists(path):
# make directories if necessary
os.makedirs(os.path.dirname(path), exist_ok=True)
# create empty library file
with open(path, "a") as file_object:
# add empty line to ensure OS recognises it as a text file
file_object.write("\n")
open_file(path)
"""Open user op-amp library file for editing."""
open_file(LIBRARY.get_user_config_filepath())
@cli.command()
@argument("term")
......
......@@ -42,6 +42,10 @@ class BaseAcAnalysis(BaseAnalysis, metaclass=abc.ABCMeta):
"""Validate circuit"""
raise NotImplementedError
@abc.abstractproperty
def prescale_value(self):
raise NotImplementedError
@property
def dim_size(self):
"""Circuit matrix dimension size
......@@ -121,10 +125,7 @@ class BaseAcAnalysis(BaseAnalysis, metaclass=abc.ABCMeta):
# create new sparse matrix
matrix = self.solver.sparse((self.dim_size, self.dim_size))
if self.prescale:
scale = 1 / self.mean_resistance
else:
scale = 1
scale = self.prescale_value
# add sources and sinks
self.set_up_sources_and_sinks()
......
......@@ -2,7 +2,6 @@ import logging
import numpy as np
from .base import BaseAcAnalysis
from ...components import Node
from ...data import NoiseSpectrum, Series
LOGGER = logging.getLogger(__name__)
......@@ -11,47 +10,27 @@ LOGGER = logging.getLogger(__name__)
class AcNoiseAnalysis(BaseAcAnalysis):
"""Small signal circuit analysis"""
def __init__(self, node, **kwargs):
# empty fields
self._node = None
self.node = node
def __init__(self, element, **kwargs):
# call parent constructor
super().__init__(**kwargs)
if not hasattr(element, "name"):
# get element name from circuit
element = self.circuit[element]
self.element = element
@property
def prescale_value(self):
# switch off for noise
return 1
def validate_circuit(self):
"""Validate circuit for noise analysis"""
# check input
if self.circuit.input_component.input_type != "noise":
raise ValueError("circuit input type must be 'noise'")
@property
def node(self):
"""Circuit noise node
Returns
-------
:class:`~.components.Node`
The circuit's noise node
"""
return self._node
@node.setter
def node(self, node):
"""Set circuit's noise node
Parameters
----------
node : :class:`~.components.Node`
The circuit's new noise node
"""
if not isinstance(node, Node):
node = Node(node)
self._node = node
def circuit_matrix(self, *args, **kwargs):
"""Calculate and return matrix used to solve for circuit noise at a \
given frequency.
......@@ -69,7 +48,7 @@ class AcNoiseAnalysis(BaseAcAnalysis):
"""Circuit noise (output) vector
This creates a vector of size nx1, where n is the number of elements in
the circuit, and sets the noise node's coefficient to 1 before
the circuit, and sets the noise element's coefficient to 1 before
returning it.
Returns
......@@ -81,14 +60,13 @@ class AcNoiseAnalysis(BaseAcAnalysis):
# create column vector
e_n = self.get_empty_results_matrix(1)
# set input to noise node
e_n[self.noise_node_index, 0] = 1
# set input to noise element
e_n[self.noise_element_index, 0] = 1
return e_n
def calculate(self):
"""Calculate noise from circuit :class:`component <.Component>` / \
:class:`node <.Node>` at a particular :class:`node <.Node>`.
"""Calculate noise from circuit elements at a particular element.
Returns
-------
......@@ -116,10 +94,10 @@ class AcNoiseAnalysis(BaseAcAnalysis):
if self.prescale:
# convert currents from natural units back to amperes
prescaler = 1 / self.mean_resistance
prescale_value = self.prescale_value
for node in self.circuit.non_gnd_nodes:
scale[self.node_matrix_index(node), 0] = 1 / prescaler
scale[self.node_matrix_index(node), 0] = 1 / prescale_value
# unscale
noise_matrix *= scale
......@@ -151,7 +129,7 @@ class AcNoiseAnalysis(BaseAcAnalysis):
# get response from this element to every other
response = noise_matrix[index, :]
# multiply response from element to noise node by noise entering
# multiply response from element to noise output element by noise entering
# at that element, for all frequencies
projected_noise = np.abs(response * spectral_density)
......@@ -159,16 +137,25 @@ class AcNoiseAnalysis(BaseAcAnalysis):
series = Series(x=self.frequencies, y=projected_noise)
# add noise function to solution
self.solution.add_noise(NoiseSpectrum(source=noise, sink=self.node,
series=series))
self.solution.add_noise(NoiseSpectrum(source=noise, sink=self.element, series=series))
if len(empty):
if empty:
LOGGER.debug("empty noise sources: %s", ", ".join([str(tf) for tf in empty]))
@property
def noise_node_index(self):
"""Noise node matrix index"""
return self.node_matrix_index(self.node)
def noise_element_index(self):
"""Noise element matrix index"""
try:
return self.component_matrix_index(self.element)
except ValueError:
pass
try:
return self.node_matrix_index(self.element)
except ValueError:
pass
raise ValueError("noise output element '%s' is not in the circuit" % self.element)
@property
def has_noise_input(self):
......
......@@ -16,6 +16,15 @@ class AcSignalAnalysis(BaseAcAnalysis):
if self.circuit.input_component.input_type not in ["voltage", "current"]:
raise ValueError("circuit input type must be either 'voltage' or 'current'")
@property
def prescale_value(self):
if self.prescale:
scale = 1 / self.mean_resistance
else:
scale = 1
return scale
def right_hand_side(self):
"""Circuit signal (input) vector.
......@@ -97,15 +106,12 @@ class AcSignalAnalysis(BaseAcAnalysis):
# create appropriate transfer function depending on input type