Commit d2f079d8 authored by Sean Leavey's avatar Sean Leavey
Browse files

Merge branch 'feature/quantities' into develop

parents 99e1a2c1 25148b06
Pipeline #71052 passed with stage
in 10 minutes and 27 seconds
......@@ -140,30 +140,28 @@ Parentheses may be used to delimit groups:
Display
~~~~~~~
The results are displayed in a table. By default, only the op-amp model names
matching a given query are displayed in the table. To add extra columns,
specify the corresponding flag as part of the call:
``--a0``
Show open loop gain.
``--gbw``
Show gain-bandwidth product.
``--delay``
Show delay.
``--vnoise``
Show flat voltage noise.
``--vcorner``
Show voltage noise corner frequency.
``--inoise``
Show flat current noise.
``--icorner``
Show current noise corner frequency.
``--vmax``
Show maximum output voltage.
``--imax``
Show maximum output current.
``--sr``
Show slew rate.
The results are displayed in a table. The rows are sorted based on the order in which the parameters
are defined in the search query, from left to right, with the leftmost parameter being sorted last.
The default sort direction is defined based on the parameter. The sort direction can be specified
explicitly as ``ASC`` (ascending) or ``DESC`` (descending) with the corresponding ``--sort``
parameter:
================== =========== =================
Flag Parameter Default direction
================== =========== =================
``--sort-a0`` ``a0`` descending
``--sort-gbw`` ``gbw`` descending
``--sort-delay`` ``delay`` ascending
``--sort-vnoise`` ``vnoise`` ascending
``--sort-vcorner`` ``vcorner`` ascending
``--sort-inoise`` ``inoise`` ascending
``--sort-icorner`` ``icorner`` ascending
``--sort-vmax`` ``vmax`` descending
``--sort-imax`` ``imax`` descending
``--sort-sr`` ``sr`` ascending
================== =========== =================
Parameters that are not explicitly searched are not ordered.
Command reference
-----------------
......
......@@ -13,6 +13,7 @@ REQUIREMENTS = [
"setuptools_scm >= 3.1.0",
"ply >= 3.11",
"Click == 7.0",
"quantiphy >= 2.5.0",
"PyYAML >= 3.13",
"graphviz >= 0.9",
]
......
"""Configuration component parser tests"""
from unittest import TestCase
from zero.config import LibraryOpAmp
class LibraryOpAmpTestCase(TestCase):
"""Library op-amp parser tests"""
def test_a0_db_scaling(self):
"""Test a0 specified in dB is correctly scaled to absolute magnitude."""
a0_abs = 1e6
opamp_a = LibraryOpAmp(a0=a0_abs)
for a0_db in ["120 dB", "120dB", "120 db", "120db", "120 DB", "120DB", "120.0 dB"]:
with self.subTest(a0_db):
opamp_b = LibraryOpAmp(a0=a0_db)
self.assertEqual(opamp_a.a0, opamp_b.a0)
......@@ -2,9 +2,9 @@
import numpy as np
from zero.data import Series, MultiNoiseDensity
from zero.misc import mag_to_db
from ..data import ZeroDataTestCase
class SeriesTestCase(ZeroDataTestCase):
"""Data series tests"""
def setUp(self):
......@@ -19,7 +19,7 @@ class SeriesTestCase(ZeroDataTestCase):
self.data_im = test_data[:, 1]
# Magnitude and phase equivalent.
self.data_mag_abs = np.abs(self.data_cplx)
self.data_mag_db = 20 * np.log10(self.data_mag_abs)
self.data_mag_db = mag_to_db(self.data_mag_abs)
self.data_phase_rad = np.angle(self.data_cplx)
self.data_phase_deg = np.degrees(self.data_phase_rad)
......
......@@ -7,16 +7,6 @@ from zero.format import Quantity
class QuantityParserTestCase(TestCase):
"""Quantity parsing tests"""
def test_invalid(self):
# invalid characters
for test_value in r" !\"€£$%^&\*\(\)\{\}\[\];:'@#~/\?><\\\|¬`":
with self.subTest(msg="Test invalid quantity", quantity=test_value):
self.assertRaisesRegex(ValueError, r"unrecognised quantity", Quantity, test_value)
# invalid strings
self.assertRaisesRegex(ValueError, r"unrecognised quantity", Quantity, "")
self.assertRaisesRegex(ValueError, r"unrecognised quantity", Quantity, "invalid")
def test_float_values(self):
"""Test parsing of float quantities"""
self.assertAlmostEqual(Quantity(1.23), 1.23)
......@@ -27,7 +17,6 @@ class QuantityParserTestCase(TestCase):
"""Test parsing of string quantities"""
self.assertAlmostEqual(Quantity("1.23"), 1.23)
self.assertAlmostEqual(Quantity("-765e3"), -765e3)
self.assertAlmostEqual(Quantity("6.3e-2.3"), 6.3 * 10 ** -2.3)
def test_string_values_with_si_scales(self):
"""Test parsing of string quantities with SI scales"""
......@@ -53,19 +42,19 @@ class QuantityParserTestCase(TestCase):
"""Test parsing of string quantities with units and SI scales"""
q = Quantity("1.23")
self.assertAlmostEqual(q, 1.23)
self.assertEqual(q.unit, None)
self.assertEqual(q.units, "")
q = Quantity("1.23 Hz")
self.assertAlmostEqual(q, 1.23)
self.assertEqual(q.unit, "Hz")
self.assertEqual(q.units, "Hz")
q = Quantity("1.69pF")
self.assertAlmostEqual(q, 1.69e-12)
self.assertEqual(q.unit, "F")
self.assertEqual(q.units, "F")
q = Quantity("3.21uH")
self.assertAlmostEqual(q, 3.21e-6)
self.assertEqual(q.unit, "H")
self.assertEqual(q.units, "H")
q = Quantity("4.88MΩ")
self.assertAlmostEqual(q, 4.88e6)
self.assertEqual(q.unit, "Ω")
self.assertEqual(q.units, "Ω")
def test_copy(self):
"""Test quantity copy constructor"""
......@@ -76,95 +65,3 @@ class QuantityParserTestCase(TestCase):
self.assertEqual(float(q), float(Quantity(q)))
# strings equal
self.assertEqual(str(q), str(Quantity(q)))
class QuantityFormatterTestCase(TestCase):
"""Quantity formatting tests"""
def test_default_format(self):
"""Test default quantities format"""
# default precision is 4
self.assertEqual(Quantity(1.23).format(), "1.2300")
self.assertEqual(Quantity("4.56k").format(), "4.5600k")
self.assertEqual(Quantity("7.89 M").format(), "7.8900M")
self.assertEqual(Quantity("1.01 GHz").format(), "1.0100 GHz")
def test_unit_format(self):
"""Test quantities with units format"""
# SI scale and unit, default precision
self.assertEqual(Quantity("1.01 GHz").format(show_unit=True, show_si=True), "1.0100 GHz")
self.assertEqual(Quantity("1.01 nHz").format(show_unit=True, show_si=True), "1.0100 nHz")
# SI scale, but no unit, default precision
self.assertEqual(Quantity("1.01 MHz").format(show_unit=False, show_si=True), "1.0100M")
self.assertEqual(Quantity("1.01 uHz").format(show_unit=False, show_si=True), "1.0100µ")
# unit, but no SI scale, default precision
self.assertEqual(Quantity("1.01 kHz").format(show_unit=True, show_si=False), "1.0100e3 Hz")
self.assertEqual(Quantity("1.01 mHz").format(show_unit=True, show_si=False), "1.0100e-3 Hz")
# no unit nor SI scale, default precision
self.assertEqual(Quantity("1.01 THz").format(show_unit=False, show_si=False), "1.0100e12")
self.assertEqual(Quantity("1.01 pHz").format(show_unit=False, show_si=False), "1.0100e-12")
# SI scale and unit, 0 decimal places
self.assertEqual(Quantity("1.01 GHz").format(show_unit=True, show_si=True, precision=0), "1 GHz")
self.assertEqual(Quantity("1.01 nHz").format(show_unit=True, show_si=True, precision=0), "1 nHz")
# SI scale, but no unit, 1 decimal place
self.assertEqual(Quantity("1.01 GHz").format(show_unit=False, show_si=True, precision=1), "1.0G")
self.assertEqual(Quantity("1.01 nHz").format(show_unit=False, show_si=True, precision=1), "1.0n")
# unit, but no SI scale, 2 decimal places
self.assertEqual(Quantity("1.01 GHz").format(show_unit=True, show_si=False, precision=2), "1.01e9 Hz")
self.assertEqual(Quantity("1.01 nHz").format(show_unit=True, show_si=False, precision=2), "1.01e-9 Hz")
# no unit nor SI scale, 3 decimal places
self.assertEqual(Quantity("1.01 GHz").format(show_unit=False, show_si=False, precision=3), "1.010e9")
self.assertEqual(Quantity("1.01 nHz").format(show_unit=False, show_si=False, precision=3), "1.010e-9")
# with decimal place move
self.assertEqual(Quantity("12345.01 GHz").format(show_unit=False, show_si=False, precision=3), "12.35e12")
self.assertEqual(Quantity("12345.01 nHz").format(show_unit=False, show_si=False, precision=3), "12.35e-6")
self.assertEqual(Quantity("0.0012345 nHz").format(show_unit=False, show_si=False, precision=3), "1.235e-12")
# SI scale and unit, full precision
self.assertEqual(Quantity("1.01 GHz").format(show_unit=True, show_si=True, precision="full"), "1.01 GHz")
self.assertEqual(Quantity("1.01 nHz").format(show_unit=True, show_si=True, precision="full"), "1.01 nHz")
# with decimal place move
self.assertEqual(Quantity("12345.01 GHz").format(show_unit=True, show_si=True, precision="full"), "12.34501 THz")
self.assertEqual(Quantity("12345.01 nHz").format(show_unit=True, show_si=True, precision="full"), "12.34501 µHz")
# SI scale, but no unit, full precision
self.assertEqual(Quantity("12.3456 GHz").format(show_unit=False, show_si=True, precision="full"), "12.3456G")
self.assertEqual(Quantity("12.3456 nHz").format(show_unit=False, show_si=True, precision="full"), "12.3456n")
# unit, but no SI scale, full precision
self.assertEqual(Quantity("123.456789 GHz").format(show_unit=True, show_si=False, precision="full"), "123.456789e9 Hz")
self.assertEqual(Quantity("123.456789 nHz").format(show_unit=True, show_si=False, precision="full"), "123.456789e-9 Hz")
# with decimal place move
self.assertEqual(Quantity("123456.789 GHz").format(show_unit=True, show_si=False, precision="full"), "123.456789e12 Hz")
self.assertEqual(Quantity("123456.789 nHz").format(show_unit=True, show_si=False, precision="full"), "123.456789e-6 Hz")
self.assertEqual(Quantity("0.00123456789 nHz").format(show_unit=True, show_si=False, precision="full"), "1.23456789e-12 Hz")
# no unit nor SI scale, full precision
self.assertEqual(Quantity("123.4567890123 GHz").format(show_unit=False, show_si=False, precision="full"), "123.4567890123e9")
self.assertEqual(Quantity("123.4567890123 nHz").format(show_unit=False, show_si=False, precision="full"), "123.4567890123e-9")
# with decimal place move
self.assertEqual(Quantity("12345.67890123 GHz").format(show_unit=False, show_si=False, precision="full"), "12.34567890123e12")
self.assertEqual(Quantity("12345.67890123 nHz").format(show_unit=False, show_si=False, precision="full"), "12.34567890123e-6")
self.assertEqual(Quantity("0.001234567890123 nHz").format(show_unit=False, show_si=False, precision="full"), "1.234567890123e-12")
# scales below f should default to exponential notation
self.assertEqual(Quantity("0.001234567890123 fHz").format(show_unit=False, show_si=False, precision="full"), "1.234567890123e-18")
self.assertEqual(Quantity("0.001234567890123 aHz").format(show_unit=False, show_si=False, precision="full"), "1.234567890123e-21")
self.assertEqual(Quantity("0.001234567890123 zHz").format(show_unit=False, show_si=False, precision="full"), "1.234567890123e-24")
self.assertEqual(Quantity("0.001234567890123 yHz").format(show_unit=False, show_si=False, precision="full"), "1.234567890123e-27")
# scales above T should default to exponential notation
self.assertEqual(Quantity("12345.67890123 THz").format(show_unit=False, show_si=False, precision="full"), "12.34567890123e15")
self.assertEqual(Quantity("12345.67890123 PHz").format(show_unit=False, show_si=False, precision="full"), "12.34567890123e18")
self.assertEqual(Quantity("12345.67890123 EHz").format(show_unit=False, show_si=False, precision="full"), "12.34567890123e21")
self.assertEqual(Quantity("12345.67890123 ZHz").format(show_unit=False, show_si=False, precision="full"), "12.34567890123e24")
self.assertEqual(Quantity("12345.67890123 YHz").format(show_unit=False, show_si=False, precision="full"), "12.34567890123e27")
......@@ -17,6 +17,9 @@ LOGGER = logging.getLogger(__name__)
CONF = ZeroConfig()
LIBRARY = OpAmpLibrary()
# Library search filter order.
LIBRARY_FILTER_CHOICE = click.Choice(("ASC", "DESC"), case_sensitive=False)
# Shared arguments:
# https://github.com/pallets/click/issues/108
......@@ -68,7 +71,7 @@ def cli():
pass
@cli.command()
@click.argument("file", type=click.File())
@click.argument("files", type=click.File(), nargs=-1, metavar="[FILE]...")
@click.option("--liso", is_flag=True, default=False, help="Simulate using LISO.")
@click.option("--liso-path", type=click.Path(exists=True, dir_okay=False), envvar='LISO_PATH',
help="Path to LISO binary. If not specified, the environment variable LISO_PATH is "
......@@ -76,7 +79,7 @@ def cli():
@click.option("--resp-scale-db/--resp-scale-abs", default=True, show_default=True,
help="Scale response y-axes in decibels.")
@click.option("--compare", is_flag=True, default=False,
help="Simulate using both this tool and LISO binary, and overlay results.")
help="Simulate using both this tool and LISO binary, and combine the results.")
@click.option("--diff", is_flag=True, default=False,
help="Show difference between results of comparison.")
@click.option("--plot/--no-plot", default=True, show_default=True, help="Display results as "
......@@ -86,79 +89,113 @@ def cli():
@click.option("--print-equations", is_flag=True, help="Print circuit equations.")
@click.option("--print-matrix", is_flag=True, help="Print circuit matrix.")
@click.pass_context
def liso(ctx, file, liso, liso_path, resp_scale_db, compare, diff, plot, save_figure,
def liso(ctx, files, liso, liso_path, resp_scale_db, compare, diff, plot, save_figure,
print_equations, print_matrix):
"""Parse and simulate LISO input or output file."""
"""Parse and simulate LISO input or output file(s). Multiple files can be specified as long as
they have compatible frequency vectors. These are all simulated and combined into one solution.
"""
state = ctx.ensure_object(State)
# Check which solutions must be computed.
compute_liso = liso or compare
compute_native = not liso or compare
if compute_liso:
# Run file with LISO and parse results.
runner = LisoRunner(script_path=file.name)
parser = runner.run(liso_path, plot=False)
liso_solution = parser.solution()
liso_solution.name = "LISO"
else:
# Parse specified file.
try:
# Try to parse as input file.
parser = LisoInputParser()
parser.parse(path=file.name)
except LisoParserError:
if not files:
click.echo("No input files provided. For help, specify --help.")
sys.exit(0)
# Determine whether to add script paths to solution names.
add_path_suffix = len(files) > 1
solutions = []
for liso_file in files:
if compute_liso:
if add_path_suffix:
name_suffix = f" {liso_file.name}"
else:
name_suffix = ""
name = f"LISO{name_suffix}"
# Run file with LISO and parse results.
runner = LisoRunner(script_path=liso_file.name)
parser = runner.run(liso_path, plot=False)
liso_solution = parser.solution()
liso_solution.name = name
else:
# Parse specified file.
try:
# Try to parse as an output file.
parser = LisoOutputParser()
parser.parse(path=file.name)
# Try to parse as input file.
parser = LisoInputParser()
parser.parse(path=liso_file.name)
except LisoParserError:
raise ValueError("cannot interpret specified file as either a LISO input or LISO "
"output file")
if compute_native:
# Build argument list.
kwargs = {"print_progress": state.verbose,
"print_equations": print_equations,
"print_matrix": print_matrix}
# Get native solution.
native_solution = parser.solution(force=True, **kwargs)
native_solution.name = "Zero"
# Determine solution to show or save.
if compare:
liso_functions = liso_solution.default_functions[Solution.DEFAULT_GROUP_NAME]
def liso_order(function):
"""Return order as specified in LISO file for specified function"""
for index, liso_function in enumerate(liso_functions):
if liso_function.meta_equivalent(function):
return index
raise ValueError(f"{function} is not in LISO solution")
# Sort native solution in the order defined in the LISO file.
native_solution.sort_functions(liso_order, default_only=True)
# Show difference before changing labels.
if diff:
# Group by meta data.
header, rows = native_solution.difference(liso_solution, defaults_only=True,
meta_only=True)
click.echo(tabulate(rows, header, tablefmt=CONF["format"]["table"]))
# Combine results from LISO and native simulations. This puts the functions from each
# solution into groups with that solution's name so we can differentiate them on the plot.
solution = native_solution.combine(liso_solution)
else:
# Plot single result.
if compute_liso:
# Use LISO's solution.
solution = liso_solution
try:
# Try to parse as an output file.
parser = LisoOutputParser()
parser.parse(path=liso_file.name)
except LisoParserError:
click.echo(f"cannot interpret {liso_file.name} as either a LISO input or LISO "
"output file", err=True)
sys.exit(1)
if compute_native:
if add_path_suffix:
name_suffix = f" {liso_file.name}"
else:
name_suffix = ""
name = f"Zero{name_suffix}"
# Build argument list.
kwargs = {"print_progress": state.verbose,
"print_equations": print_equations,
"print_matrix": print_matrix}
# Get native solution.
native_solution = parser.solution(force=True, **kwargs)
native_solution.name = name
# Determine solution to show or save.
if compare:
liso_functions = liso_solution.default_functions[Solution.DEFAULT_GROUP_NAME]
def liso_order(function):
"""Return order as specified in LISO file for specified function"""
for index, liso_function in enumerate(liso_functions):
if liso_function.meta_equivalent(function):
return index
click.echo(f"{function} is not in LISO solution", err=True)
sys.exit(1)
# Sort native solution in the order defined in the LISO file.
native_solution.sort_functions(liso_order, default_only=True)
# Show difference before changing labels.
if diff:
# Group by meta data.
header, rows = native_solution.difference(liso_solution, defaults_only=True,
meta_only=True)
click.echo(tabulate(rows, header, tablefmt=CONF["format"]["table"]))
# Combine results from LISO and native simulations. This puts the functions from each
# solution into groups with that solution's name so we can differentiate them on the
# plot.
solution = native_solution.combine(liso_solution)
else:
# Use native solution.
solution = native_solution
# Plot single result.
if compute_liso:
# Use LISO's solution.
solution = liso_solution
else:
# Use native solution.
solution = native_solution
solutions.append(solution)
solution = solutions[0]
if len(solutions) > 1:
# Combine all simulated solutions.
solution = solution.combine(*solutions[1:])
# Determine whether to generate plot.
generate_plot = plot or save_figure
......@@ -229,17 +266,19 @@ def library_show(paged):
@library.command("search")
@click.argument("query")
@click.option("--a0", is_flag=True, default=False, help="Show open loop gain.")
@click.option("--gbw", is_flag=True, default=False, help="Show gain-bandwidth product.")
@click.option("--vnoise", is_flag=True, default=False, help="Show flat voltage noise.")
@click.option("--vcorner", is_flag=True, default=False, help="Show voltage noise corner frequency.")
@click.option("--inoise", is_flag=True, default=False, help="Show flat current noise.")
@click.option("--icorner", is_flag=True, default=False, help="Show current noise corner frequency.")
@click.option("--vmax", is_flag=True, default=False, help="Show maximum output voltage.")
@click.option("--imax", is_flag=True, default=False, help="Show maximum output current.")
@click.option("--sr", is_flag=True, default=False, help="Show slew rate.")
@click.option("--sort-a0", type=LIBRARY_FILTER_CHOICE, default="DESC", show_default=True)
@click.option("--sort-gbw", type=LIBRARY_FILTER_CHOICE, default="DESC", show_default=True)
@click.option("--sort-delay", type=LIBRARY_FILTER_CHOICE, default="ASC", show_default=True)
@click.option("--sort-vnoise", type=LIBRARY_FILTER_CHOICE, default="ASC", show_default=True)
@click.option("--sort-vcorner", type=LIBRARY_FILTER_CHOICE, default="ASC", show_default=True)
@click.option("--sort-inoise", type=LIBRARY_FILTER_CHOICE, default="ASC", show_default=True)
@click.option("--sort-icorner", type=LIBRARY_FILTER_CHOICE, default="ASC", show_default=True)
@click.option("--sort-vmax", type=LIBRARY_FILTER_CHOICE, default="DESC", show_default=True)
@click.option("--sort-imax", type=LIBRARY_FILTER_CHOICE, default="DESC", show_default=True)
@click.option("--sort-sr", type=LIBRARY_FILTER_CHOICE, default="ASC", show_default=True)
@click.option("--paged", is_flag=True, default=False, help="Print results with paging.")
def library_search(query, a0, gbw, vnoise, vcorner, inoise, icorner, vmax, imax, sr, paged):
def library_search(query, sort_a0, sort_gbw, sort_delay, sort_vnoise, sort_vcorner, sort_inoise,
sort_icorner, sort_vmax, sort_imax, sort_sr, paged):
"""Search Zero op-amp library.
Op-amp parameters listed in the library can be searched:
......@@ -260,60 +299,38 @@ def library_search(query, a0, gbw, vnoise, vcorner, inoise, icorner, vmax, imax,
The query engine supports arbitrary expressions.
Example: all op-amps with noise less than 10 nV/sqrt(Hz) and corner frequency
below 10 Hz:
The 'a0' parameter can be specified in magnitude or decibels. For decibels, append 'dB' (case
insensitive) to the value.
vnoise < 10n & vcorner < 10
The results are sorted sequentially in the order that each parameter appears in the
search query (left to right). The sort direction (descending or ascending) depends on the type
of parameter. The sort direction for parameter 'X' can be overridden using the corresponding
'--sort-X' flag. Specify 'ASC' for ascending and 'DESC' for descending order.
"""
echo = click.echo_via_pager if paged else click.echo
engine = LibraryQueryEngine()
# build parameter list
params = []
if a0:
params.append("a0")
if gbw:
params.append("gbw")
if vnoise:
params.append("vnoise")
if vcorner:
params.append("vcorner")
if inoise:
params.append("inoise")
if icorner:
params.append("icorner")
if vmax:
params.append("vmax")
if imax:
params.append("imax")
if sr:
params.append("sr")
# get results
devices = engine.query(query)
sort_order = {"a0": sort_a0 == "DESC", "gbw": sort_gbw == "DESC", "delay": sort_delay == "DESC",
"vnoise": sort_vnoise == "DESC", "vcorner": sort_vcorner == "DESC",
"inoise": sort_inoise == "DESC", "icorner": sort_icorner == "DESC",
"vmax": sort_vmax == "DESC", "imax": sort_imax == "DESC", "sr": sort_sr == "DESC"}
# Get results.
devices = engine.query(query, sort_order=sort_order)
if not devices:
click.echo("No op-amps found", err=True)
sys.exit()
sys.exit(1)
nmodel = len(devices)
if nmodel == 1:
opstr = "op-amp"
else:
opstr = "op-amps"
click.echo(f"{nmodel} {opstr} found:")
header = ["Model"] + params
rows = []
for device in devices:
row = [device.model]
row.extend([str(getattr(device, param)) for param in params])
rows.append(row)
echo(tabulate(rows, header, tablefmt=CONF["format"]["table"]))
rows.append([str(getattr(device, param)) for param in engine.parameters])
table = tabulate(rows, engine.parameters, tablefmt=CONF["format"]["table"])
if paged:
click.echo_via_pager(table)
else:
click.echo(table)
@cli.group()
def config():
......
......@@ -490,7 +490,7 @@ class Inductor(PassiveComponent):
coupling_factor = self.coupling_factors[other]
mutual_inductance = coupling_factor * np.sqrt(self.inductance * other.inductance)
return Quantity(mutual_inductance, unit=self.DISPLAY_UNIT)
return Quantity(mutual_inductance, units=self.DISPLAY_UNIT)
def impedance_from(self, other, frequency):
"""Calculate the impedance this inductor has due to the specified coupled inductor
......
......@@ -5,6 +5,7 @@ import numpy as np
from .base import BaseConfig
from ..format import Quantity
from ..misc import db_to_mag
LOGGER = logging.getLogger(__name__)
......@@ -121,40 +122,21 @@ class OpAmpLibrary(BaseConfig):
if "zeros" in data and data["zeros"] is not None:
for freq in data["zeros"]:
zeros.extend(self._parse_freq_str(freq))
poles = np.array(poles)
zeros = np.array(zeros)
# Build op-amp data dict with poles and zeros as entries.
class_data = {"zeros": zeros, "poles": poles}
# Add other op-amp data.
if "a0" in data:
class_data["a0"] = Quantity(data["a0"])
if "gbw" in data:
class_data["gbw"] = Quantity(data["gbw"], "Hz")
if "delay" in data:
class_data["delay"] = Quantity(data["delay"], "s")
if "vnoise" in data:
class_data["vnoise"] = Quantity(data["vnoise"], "V/sqrt(Hz)")
if "vcorner" in data: