Commit 623a9034 authored by Sean Leavey's avatar Sean Leavey

Merge branch 'feature/click' into develop

parents 8f52ce5f e995a598
Pipeline #35829 passed with stage
in 1 minute and 36 seconds
......@@ -38,6 +38,7 @@ stages:
- chmod +x fil_static
- cd ..
- export LISO_DIR=$(pwd)/liso
- export LISO_PATH=$LISO_DIR/fil_static
- python --version
- apt update -qy
- apt install --assume-yes python-pip
......
......@@ -2,8 +2,8 @@ import logging
import locale
from pkg_resources import get_distribution, DistributionNotFound
# make Circuit class available from main package
from .circuit import Circuit
PROGRAM = "circuit"
DESCRIPTION = "Linear circuit simulator"
# get version
try:
......@@ -12,18 +12,9 @@ except DistributionNotFound:
# packaging resources are not installed
__version__ = '?'
PROGRAM = "circuit"
DESCRIPTION = "Linear circuit simulator"
# suppress warnings when the user code does not include a handler
logging.getLogger().addHandler(logging.NullHandler())
# use default locale (required for number formatting in log warnings)
locale.setlocale(locale.LC_ALL, "")
try:
from matplotlib import rcParams
from circuit.config import CircuitConfig
from .config import CircuitConfig
# get config
CONF = CircuitConfig()
......@@ -34,10 +25,30 @@ except ImportError:
# matplotlib and/or numpy not installed
pass
def logging_on(level=logging.DEBUG, format_str="%(name)-25s - %(levelname)-8s - %(message)s"):
"""Enable logging to stdout"""
handler = logging.StreamHandler()
# Make Circuit class available from main package.
# This is placed here because dependent imports need the code above.
from .circuit import Circuit
# suppress warnings when the user code does not include a handler
logging.getLogger().addHandler(logging.NullHandler())
# use default locale (required for number formatting in log warnings)
locale.setlocale(locale.LC_ALL, "")
def add_log_handler(logger, handler=None, format_str="%(name)-25s - %(levelname)-8s - %(message)s"):
if handler is None:
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(format_str))
logger = logging.getLogger(__name__)
logger.addHandler(handler)
# create base logger
LOGGER = logging.getLogger(__name__)
add_log_handler(LOGGER)
def set_log_verbosity(level, logger=None):
"""Enable logging to stdout with a certain level"""
if logger is None:
logger = LOGGER
logger.setLevel(level)
This diff is collapsed.
......@@ -31,6 +31,8 @@ class BaseAcAnalysis(BaseAnalysis, metaclass=abc.ABCMeta):
# empty fields
self._solution = None
self._node_sources = None
self._node_sinks = None
# validate the circuit for the current analysis
self.validate_circuit()
......@@ -61,7 +63,7 @@ class BaseAcAnalysis(BaseAnalysis, metaclass=abc.ABCMeta):
@property
def solution(self):
if self._solution is None:
self._solution = Solution(self.circuit, self.frequencies)
self._solution = Solution(self.frequencies)
return self._solution
......@@ -197,6 +199,9 @@ class BaseAcAnalysis(BaseAnalysis, metaclass=abc.ABCMeta):
# right hand side to solve against
rhs = self.right_hand_side()
# debug info
LOGGER.debug("Matrix prescaling: %s", self.prescale)
# frequency loop
for index, frequency in enumerate(freq_gen):
# get matrix for this frequency, converting to CSR format for
......
......@@ -109,10 +109,7 @@ class Component(metaclass=abc.ABCMeta):
return self.label()
def __eq__(self, other):
if hasattr(other, 'name'):
return self.name == other.name
return False
return self.name == getattr(other, "name", None)
def __hash__(self):
"""Components uniquely defined by their name"""
......@@ -707,12 +704,22 @@ class Noise(metaclass=abc.ABCMeta):
def label(self):
return NotImplemented
def _meta_data(self):
"""Meta data used to provide hash."""
return tuple(self.label())
def __str__(self):
return self.label()
def __repr__(self):
return str(self)
def __eq__(self, other):
return hash(self) == hash(other)
def __hash__(self):
return hash(self._meta_data())
class ComponentNoise(Noise, metaclass=abc.ABCMeta):
"""Component noise spectral density.
......@@ -733,6 +740,10 @@ class ComponentNoise(Noise, metaclass=abc.ABCMeta):
def spectral_density(self, frequencies):
return self.function(component=self.component, frequencies=frequencies)
def _meta_data(self):
"""Meta data used to provide hash."""
return super()._meta_data(), self.component
class NodeNoise(Noise, metaclass=abc.ABCMeta):
"""Node noise spectral density.
......@@ -756,6 +767,10 @@ class NodeNoise(Noise, metaclass=abc.ABCMeta):
def spectral_density(self, *args, **kwargs):
return self.function(node=self.node, *args, **kwargs)
def _meta_data(self):
"""Meta data used to provide hash."""
return super()._meta_data(), self.node, self.component
class VoltageNoise(ComponentNoise):
SUBTYPE = "voltage"
......@@ -782,6 +797,10 @@ class JohnsonNoise(VoltageNoise):
def label(self):
return "R(%s)" % self.component.name
def _meta_data(self):
"""Meta data used to provide hash."""
return super()._meta_data(), self.resistance
class CurrentNoise(NodeNoise):
SUBTYPE = "current"
......
......@@ -4,11 +4,12 @@ import os.path
import abc
import logging
import re
import numpy as np
from configparser import ConfigParser
import numpy as np
import pkg_resources
import appdirs
import click
from . import PROGRAM
from .format import Quantity
LOGGER = logging.getLogger(__name__)
......@@ -25,7 +26,7 @@ class SingletonAbstractMeta(abc.ABCMeta):
if cls not in cls._SINGLETON_REGISTRY:
# create new instance
cls._SINGLETON_REGISTRY[cls] = super().__call__(*args, **kwargs)
return cls._SINGLETON_REGISTRY[cls]
class BaseConfig(ConfigParser, metaclass=SingletonAbstractMeta):
......@@ -81,7 +82,7 @@ class BaseConfig(ConfigParser, metaclass=SingletonAbstractMeta):
:rtype: str
"""
config_dir = appdirs.user_config_dir("circuit.py")
config_dir = click.get_app_dir(PROGRAM)
config_file = os.path.join(config_dir, cls.CONFIG_FILENAME)
return config_file
......@@ -296,7 +297,7 @@ class OpAmpLibrary(BaseConfig):
# generate complex frequencies from the list and combine them into one list
frequencies = []
for token in freq_tokens:
frequencies.extend(self._parse_freq_str(token))
......
This diff is collapsed.
LISO_PATH_ENV_VAR = "LISO_PATH"
# LISO tools
from .base import LisoParserError
from .input import LisoInputParser
from .output import LisoOutputParser
from .runner import LisoRunner
\ No newline at end of file
from .runner import LisoRunner
......@@ -10,6 +10,7 @@ import numpy as np
from ..circuit import Circuit, ComponentNotFoundError
from ..components import Node
from ..analysis import AcSignalAnalysis, AcNoiseAnalysis
from ..data import MultiNoiseSpectrum
from ..format import Quantity
LOGGER = logging.getLogger(__name__)
......@@ -68,13 +69,12 @@ class LisoParser(metaclass=abc.ABCMeta):
# noise sources to calculate
self._noise_sources = None
# noise sum flags
self._noise_sum_requested = False # a noise sum should be computed
self._noise_sum_present = False # a noise sum is present in the data
# circuit solution
self._solution = None
# flag for when noise sum must be computed when building solution
self._noise_sum_to_be_computed = False
# create lexer and parser handlers
self.lexer = lex.lex(module=self)
self.parser = yacc.yacc(module=self)
......@@ -180,11 +180,11 @@ class LisoParser(metaclass=abc.ABCMeta):
@property
def n_displayed_noise(self):
return len(self.displayed_noise_sources)
return len(self.displayed_noise_objects)
@property
def n_summed_noise(self):
return len(self.summed_noise_sources)
return len(self.summed_noise_objects)
def parse(self, text=None, path=None):
if text is None and path is None:
......@@ -219,35 +219,6 @@ class LisoParser(metaclass=abc.ABCMeta):
"""Child classes must implement error handler"""
raise NotImplementedError
def show(self, *args, **kwargs):
"""Show LISO results"""
# build circuit if necessary
self.build()
if not self.plottable:
LOGGER.warning("nothing to show")
# get solution
solution = self.solution(*args, **kwargs)
# draw plots
if self.output_type == "tf":
solution.plot_tfs(sources=self.default_tf_sources(), sinks=self.default_tf_sinks())
elif self.output_type == "noise":
sum_kwargs = {}
if self._noise_sum_requested:
# calculate and plot noise sum
sum_kwargs["compute_sum_sources"] = self.summed_noise_sources
solution.plot_noise(sources=self.displayed_noise_sources,
show_sums=self._noise_sum_present, **sum_kwargs)
else:
raise Exception("unrecognised output type")
# display plots
solution.show()
def default_tf_sources(self):
"""Default transfer function sources"""
# note: this cannot be a property, otherwise lex will execute the property before
......@@ -270,16 +241,50 @@ class LisoParser(metaclass=abc.ABCMeta):
"""
return [element.element for element in self.tf_outputs]
def solution(self, force=False, **kwargs):
"""Get the solution to the analysis defined in the parsed file"""
def solution(self, force=False, set_default_plots=True, **kwargs):
"""Get the solution to the analysis defined in the parsed file.
Parameters
----------
force : :class:`bool`
Whether to force the solution to be recomputed if already generated.
set_default_plots : :class:`bool`
Set the plots defined in the LISO file as defaults.
"""
# build circuit if necessary
self.build()
if not self._solution or force:
self._solution = self._run(**kwargs)
if self._noise_sum_to_be_computed:
# find spectra in solution
sum_spectra = self._solution.filter_noise(sources=self.summed_noise_objects)
# create overall spectrum
sum_spectrum = MultiNoiseSpectrum(sink=self.noise_output_node,
constituents=sum_spectra)
# build noise sum and show by default
self._solution.add_noise_sum(sum_spectrum, default=True)
if set_default_plots:
self._set_default_plots()
return self._solution
def _set_default_plots(self):
# set default plots
if self.output_type == "tf":
default_tfs = self._solution.filter_tfs(sources=self.default_tf_sources(),
sinks=self.default_tf_sinks())
for tf in default_tfs:
self._solution.set_tf_as_default(tf)
elif self.output_type == "noise":
default_spectra = self._solution.filter_noise(sources=self.displayed_noise_objects)
for spectrum in default_spectra:
self._solution.set_noise_as_default(spectrum)
def _run(self, print_equations=False, print_matrix=False, stream=sys.stdout, **kwargs):
# build circuit if necessary
self.build()
......@@ -304,7 +309,6 @@ class LisoParser(metaclass=abc.ABCMeta):
def build(self):
"""Build circuit if not yet built"""
if not self._circuit_built:
self._do_build()
......@@ -353,8 +357,8 @@ class LisoParser(metaclass=abc.ABCMeta):
# check if noise sources exist
try:
_ = self.displayed_noise_sources
_ = self.summed_noise_sources
_ = self.displayed_noise_objects
_ = self.summed_noise_objects
except ComponentNotFoundError as e:
self.p_error("noise source '%s' is not present in the circuit" % e.name)
......@@ -395,13 +399,13 @@ class LisoParser(metaclass=abc.ABCMeta):
@property
@abc.abstractmethod
def displayed_noise_sources(self):
"""Displayed noise sources, not including sums"""
def displayed_noise_objects(self):
"""Displayed noise objects, not including sums"""
raise NotImplementedError
@property
@abc.abstractmethod
def summed_noise_sources(self):
def summed_noise_objects(self):
"""Noise sources included in displayed noise sum outputs"""
raise NotImplementedError
......
......@@ -78,6 +78,9 @@ class LisoInputParser(LisoParser):
self._noise_defs = [] # displayed noise
self._noisy_extra_defs = [] # extra noise to include in "sum" in addition to displayed noise
# noise sum request
self._noise_sum_to_be_computed = False
super().__init__(*args, **kwargs)
def _do_build(self):
......@@ -125,7 +128,7 @@ class LisoInputParser(LisoParser):
scales=self._output_all_opamps_scales)
self.add_tf_output(sink)
def _get_noise_sources(self, definitions):
def _get_noise_objects(self, definitions):
"""Get noise objects for the specified raw noise defintions"""
sources = set()
......@@ -161,7 +164,7 @@ class LisoInputParser(LisoParser):
sources.update(self._get_component_noise(resistor))
elif component_name == "sum":
# show sum of circuit noises
self._noise_sum_requested = True
self._noise_sum_to_be_computed = True
else:
# individual component
component = self.circuit[component_name]
......@@ -210,21 +213,21 @@ class LisoInputParser(LisoParser):
return noise
@property
def displayed_noise_sources(self):
def displayed_noise_objects(self):
"""Noise sources to be plotted"""
return self._get_noise_sources(self._noise_defs)
return self._get_noise_objects(self._noise_defs)
@property
def summed_noise_sources(self):
def summed_noise_objects(self):
"""Noise sources included in the sum column"""
# check "sum" is not present in noisy definitions
if "sum" in [definition[0].split(":")[0] for definition in self._noisy_extra_defs]:
self.p_error("cannot specify 'sum' as noisy source")
sum_sources = self._get_noise_sources(self._noisy_extra_defs)
sum_sources = self._get_noise_objects(self._noisy_extra_defs)
# add displayed noise sources if not already present
sum_sources.update(self.displayed_noise_sources)
sum_sources.update(self.displayed_noise_objects)
return sum_sources
......
......@@ -4,7 +4,7 @@ import logging
import numpy as np
from ..solution import Solution
from ..data import (Series, TransferFunction, NoiseSpectrum, SumNoiseSpectrum)
from ..data import Series, TransferFunction, NoiseSpectrum, MultiNoiseSpectrum
from ..format import Quantity
from ..components import OpAmp
from .base import LisoParser, LisoOutputVoltage, LisoOutputCurrent, LisoParserError
......@@ -140,7 +140,7 @@ class LisoOutputParser(LisoParser):
self._data = None
def _build_solution(self):
self._solution = Solution(self.circuit, self.frequencies)
self._solution = Solution(self.frequencies)
if self.output_type == "tf":
self._build_tfs()
......@@ -256,31 +256,27 @@ class LisoOutputParser(LisoParser):
# must be a resistor
noise = component.johnson_noise
# noise should always be in the noise source list
#assert noise in self.noise_sources
# create noise spectrum
spectrum = NoiseSpectrum(source=noise, sink=sink, series=series)
self._solution.add_noise(spectrum)
# add sum column if present
# generate sum if present
if self._source_sum_index is not None:
# set flag
self._noise_sum_present = True
# get sources contributing to sum
sources = self.summed_noise_sources
sources = self.summed_noise_objects
# get data
series = Series(x=self.frequencies, y=self._data[:, self._source_sum_index])
# create sum noise
spectrum = SumNoiseSpectrum(sources=sources, sink=sink, series=series)
# create and store sum noise
sum_noise = MultiNoiseSpectrum(sources=sources, sink=sink, series=series)
self._solution.add_noise_sum(sum_noise, default=True)
self._solution.add_noise(spectrum)
# flag that noise sum must be generated for any future native runs of this circuit
self._noise_sum_to_be_computed = True
def _get_noise_sources(self, definitions):
def _get_noise_objects(self, definitions):
"""Get noise objects for the specified raw noise defintions"""
sources = set()
......@@ -316,14 +312,14 @@ class LisoOutputParser(LisoParser):
return sources
@property
def displayed_noise_sources(self):
"""Noise sources to be plotted"""
return self._get_noise_sources(self._noise_defs)
def displayed_noise_objects(self):
"""Noise objects to be plotted"""
return self._get_noise_objects(self._noise_defs)
@property
def summed_noise_sources(self):
def summed_noise_objects(self):
"""Noise sources included in the sum column"""
return self._get_noise_sources(self._noisy_defs)
return self._get_noise_objects(self._noisy_defs)
def t_ANY_resistors(self, t):
# match start of resistor section
......
"""Tools for running LISO directly"""
import sys
import os
import logging
from tempfile import NamedTemporaryFile
import subprocess
import shutil
from . import LISO_PATH_ENV_VAR
from .base import LisoParserError
from .output import LisoOutputParser
......@@ -14,77 +13,70 @@ LOGGER = logging.getLogger(__name__)
class LisoRunner:
"""LISO runner"""
# LISO binary names, in order of preference
LISO_BINARY_NAMES = {"nix": ["fil_static", "fil"], # Linux / OSX
"win": ["fil.exe"]} # Windows
def __init__(self, script_path=None):
"""LISO runner
Parameters
----------
script_path : :class:`str`
Path to LISO script to run.
"""
def __init__(self, script_path):
self.script_path = script_path
# defaults
self._liso_path = None
def run(self, liso_plot=False, liso_parse=True, liso_path=None, output_path=None):
def run(self, liso_path=None, output_path=None, plot=False, parse_output=True):
"""Run LISO script using a local LISO binary and handle the results
Parameters
----------
liso_plot : :class:`bool`, optional
Plot the results using LISO.
liso_parse : :class:`bool`, optional
Parse the output from LISO.
liso_path : :class:`str`, optional
Path to local LISO binary. If not specified, this program will attempt to find it
automatically.
Path to local LISO binary. If not specified, the value of the environment variable
defined in .liso.LISO_PATH_ENV_VAR is used.
output_path : :class:`str`, optional
Path to save LISO output file to. If not specified, LISO's default is used.
Path to save LISO output file to.
plot : :class:`bool`, optional
Plot the results using LISO.
parse_output : :class:`bool`, optional
Parse the output from LISO.
Returns
-------
:class:`.LisoOutputParser`
The parsed LISO output.
Raises
------
ValueError
If the LISO path cannot be determined.
"""
self.liso_path = liso_path
if liso_path is None:
# look for environment variable
liso_path = os.getenv(LISO_PATH_ENV_VAR)
if liso_path is None:
raise ValueError("LISO path cannot be determined. Set the environment variable "
"'%s' to the LISO binary path." % LISO_PATH_ENV_VAR)
if not output_path:
if output_path is None:
# use temporary file
temp_file = NamedTemporaryFile()
output_path = temp_file.name
return self._liso_result(self.script_path, output_path, liso_parse, liso_plot)
def _liso_result(self, script_path, output_path, parse, plot):
"""Get LISO results
Parameters
----------
script_path : :class:`str`
Path to the LISO ".fil" file.
output_path : :class:`str`
Path to LISO ".out" file to be created.
parse : :class:`bool`
Parse the output from LISO.
plot : :class:`bool`
Ask LISO to plot the result instead of this program.
Returns
-------
:class:`.OutputParser` or None
Parsed LISO output, if requested, otherwise None.
"""
self._run_liso_process(script_path, output_path, plot)
# run LISO
self._run_liso_process(liso_path, output_path, plot)
if parse:
if parse_output:
parser = LisoOutputParser()
parser.parse(path=output_path)
else:
parser = None
if output_path is None:
temp_file.close()
return parser
def _run_liso_process(self, script_path, output_path, plot):
input_path = os.path.abspath(script_path)
def _run_liso_process(self, liso_path, output_path, plot):
input_path = os.path.abspath(self.script_path)
if not os.path.exists(input_path):
raise Exception("input file %s does not exist" % input_path)
......@@ -96,7 +88,6 @@ class LisoRunner:
if not plot:
flags.append("-n")
liso_path = self.liso_path
LOGGER.debug("running LISO binary at %s", liso_path)
# run LISO
......@@ -104,59 +95,10 @@ class LisoRunner:
stderr=subprocess.PIPE)
if result.returncode != 0:
raise LisoError(result.stderr, script_path=script_path)
raise LisoError(result.stderr, script_path=self.script_path)
return result
@property
def liso_path(self):
if self._liso_path is not None:
return self._liso_path
liso_path = None
# try environment variable
try:
liso_path = self.find_liso(os.environ["LISO_DIR"])
except (KeyError, FileNotFoundError):
# no environment variable set or LISO not found in specified directory
# try searching path
for command in self.LISO_BINARY_NAMES[self.platform_key]:
path = shutil.which(command)
if path is not None:
liso_path = path
break
if liso_path is None:
raise FileNotFoundError("environment variable \"LISO_DIR\" must point to "
"the directory containing the LISO binary, or the "
"LISO binary must be available on the system PATH "
"(and executable)")
return liso_path
@liso_path.setter
def liso_path(self, path):
self._liso_path = path
@property
def platform_key(self):
if sys.platform.startswith("linux") or sys.platform.startswith("darwin"):
return "nix"
elif sys.platform.startswith("win32"):
return "win"
raise EnvironmentError("unrecognised operating system")
def find_liso(self, directory):
for filename in self.LISO_BINARY_NAMES[self.platform_key]:
path = os.path.join(directory, filename)
if os.path.isfile(path):
return path
raise FileNotFoundError("no appropriate LISO binary found")
class LisoError(Exception):
def __init__(self, message, script_path=None, *args, **kwargs):
......@@ -179,7 +121,7 @@ class LisoError(Exception):
# attempt to parse as input
try:
parser.parse(path=script_path)
parser.parse(script_path)
is_output = True
except (IOError, LisoParserError):
......
This diff is collapsed.
######################
Command line interface
######################
`Circuit` provides a command line interface to perform some common tasks, mainly focused on the
running, display and comparison of LISO scripts.
.. hint::
Also see the documentation on :ref:`LISO compatibility <liso/index:LISO Compatibility>`.
====================
Command line options
====================
.. click:: circuit.__main__:cli
:prog: circuit
:show-nested: