Commit 9aabdb74 authored by Sean Leavey's avatar Sean Leavey
Browse files

Merge branch 'release/0.7.0'

parents 00f14f2a df6fc89c
Pipeline #68743 passed with stages
in 12 minutes and 42 seconds
......@@ -6,5 +6,8 @@
"editor.insertSpaces": true,
"[markdown]": {
"files.trimTrailingWhitespace": false
}
},
"restructuredtext.linter.extraArgs": [
"--max-line-length 100",
],
}
......@@ -4,7 +4,7 @@
import sys
from zero.liso import LisoInputParser
# create parser
# Create parser.
parser = LisoInputParser()
base_circuit = """
......@@ -21,32 +21,34 @@ freq log 100k 100M 1000
uoutput nout
"""
# parse base circuit
# Parse the base circuit.
parser.parse(base_circuit)
# set input to low frequency port
# Set the circuit input to the low frequency port.
parser.parse("uinput nlf 50")
# ground unused input
# Ground the unused input.
parser.parse("r nrfsrc 5 nrf gnd")
# calculate solution
# Calculate the solution.
solutionlf = parser.solution()
solutionlf.name = "LF Circuit"
# reset parser state
# Reset the parser's state.
parser.reset()
# parse base circuit
# Parse the base circuit.
parser.parse(base_circuit)
# set input to radio frequency port
# Set the input to the radio frequency port.
parser.parse("uinput nrf 50")
# ground unused input
# Ground the unused input.
parser.parse("r nlfsrc 5 nlf gnd")
# calculate solution
# Calculate the solution.
solutionrf = parser.solution()
solutionrf.name = "RF Circuit"
# combine solutions
# Combine the solutions. By default, this keeps the functions from each source solution in different
# groups in the resulting solution. This makes the plot show the functions with different styles and
# shows the source solution's name as a suffix on each legend label.
solution = solutionlf.combine(solutionrf)
# plot
# Plot.
plot = solution.plot_responses()
solution.save_figure(plot, sys.argv[1])
.. include:: /defs.txt
.. currentmodule:: zero.analysis.ac.noise
Small AC noise analysis
=======================
Linear AC noise analysis.
The small signal AC noise analysis calculates the :ref:`noise spectral densities <data/index:Noise
spectral densities>` at a particular :class:`.Node` or :class:`.Component` within a circuit due to
noise sources within the circuit, assuming that the noise is small enough not to influence the
operating point and gain of the circuit.
Generating noise sums
---------------------
Incoherent noise sums can be created as part of the analysis and added to the :class:`.Solution`.
This is governed by the ``incoherent_sum`` parameter of :meth:`~.AcNoiseAnalysis.calculate`.
Setting ``incoherent_sum`` to ``True`` results in the incoherent sum of all noise in the circuit at
the specified noise sink being calculated and added as a single function to the solution.
Alternatively, ``incoherent_sum`` can be specified as a :class:`dict` containing legend labels as
keys and sequences of noise spectra as values. The noise spectra can either be
:class:`.NoiseDensity` objects or :ref:`noise specifier strings <solution/index:Specifying noise
sources and sinks>` as supported by :meth:`.Solution.get_noise`. The values may alternatively be the
strings "all", "allop" or "allr" to compute noise from all components, all op-amps and all
resistors, respectively.
Sums are plotted in shades of grey determined by the plotting configuration's
``sum_greyscale_cycle_start``, ``sum_greyscale_cycle_stop`` and ``sum_greyscale_cycle_count``
values.
Examples
~~~~~~~~
Add a total incoherent sum to the solution:
.. code-block:: python
solution = analysis.calculate(frequencies=frequencies, input_type="voltage", node="n1",
sink="nout", incoherent_sum=True)
Add an incoherent sum of all resistor noise:
.. code-block:: python
solution = analysis.calculate(frequencies=frequencies, input_type="voltage", node="n1",
sink="nout", incoherent_sum={"resistors": "allr"})
Add incoherent sums of all resistor and op-amp noise:
.. code-block:: python
# Shorthand syntax.
solution = analysis.calculate(frequencies=frequencies, input_type="voltage", node="n1",
sink="nout", incoherent_sum={"resistors": "allr",
"op-amps": "allop"})
# Alternatively specify components directly using noise specifiers.
solution = analysis.calculate(frequencies=frequencies, input_type="voltage", node="n1",
sink="nout", incoherent_sum={"sum": ["R(r1)", "V(op1)"]})
Referring noise to the input
----------------------------
It is often desirable to refer the noise at a node or component to the input. This is particularly
useful when modelling readout circuits (e.g. for photodetectors), where the input referred noise
shows the smallest equivalent signal spectral density that can be detected above the noise.
Noise analyses can refer noise at a node or component to the input by setting the ``input_refer``
flag to ``True`` in :meth:`~.AcNoiseAnalysis.calculate`, which makes |Zero| apply a response
function (from the noise sink to the input) to the noise computed at the noise sink. The resulting
noise has its ``sink`` property changed to the input. If ``input_type`` was set to ``voltage``, this
is the input node; whereas if ``input_type`` was set to ``current``, this is the input component.
.. note::
The input referring response function is obtained by performing a separate :ref:`signal analysis
<analyses/ac/signal:Small AC signal analysis>` with the same circuit as the noise analysis. The
response from the input to the sink is then extracted and inverted to give the response from the
sink to the input. The noise at the sink in the noise analysis is then multiplied by this input
referring response function.
......@@ -3,4 +3,8 @@
Small AC signal analysis
========================
Linear AC response analysis.
The small AC signal analysis calculates the signal at all :class:`nodes <.Node>` and
:class:`components <.Component>` within a circuit due to either a voltage or a current applied to
the circuit's input. The input is unity, meaning that the resulting signals represent the
:ref:`responses <data/index:Responses>` from the input to each node or component. The analysis
assumes that the input is small enough not to influence the operating point and gain of the circuit.
......@@ -108,6 +108,13 @@ they occur:
│ nin to no (V/V) │ 1.04e-08 (f = 79.433 kHz) │ 9.54e-10 (f = 79.433 kHz) │
╘══════════════════╧═══════════════════════════════╧═══════════════════════════════╛
Scaling response plots
----------------------
Responses can be scaled in either decibels or absolute values. The default is to scale in decibels
(``--resp-scale-db``, on by default), but this can be switched off with the ``--resp-scale-abs``
flag.
Saving figures
--------------
......
......@@ -8,32 +8,41 @@ Data containers
.. code-block:: python
>>> from zero.data import Response, NoiseDensity
>>> from zero.data import Series, Response, NoiseDensity
|Zero| :ref:`analysis <analyses/index:Analyses>` results (responses and noise spectra) are
stored within `function` containers. These are relatively low level objects that hold each
function's data, its frequency axis, and any meta data produced by the analysis. These objects are
able to plot themselves when provided a figure to draw to. They also contain logic to compare
themselves to other functions, to check for equivalency.
|Zero| :ref:`analysis <analyses/index:Analyses>` results (responses and noise spectra) are stored
within `function` containers. These are relatively low level objects that hold each function's data
(within a :ref:`series <data/index:Series>`), its frequency axis, and any meta data produced by the
analysis. These objects are able to plot themselves when provided a figure to draw to. They also
contain logic to compare themselves to other functions, to check for equivalency.
In normal circumstances, you should not need to directly interact with these objects; rather, you
can plot and save their underlying data using a :ref:`Solution <solution/index:Solutions>`.
Responses
Series
------
Underlying function data is stored in a :class:`.Series`. This contains two dimensional data. Series
support basic mathematical operations such as multiplication, division and inversion.
Functions
---------
Responses
~~~~~~~~~
:class:`Responses <.data.Response>` contain the response of a component or node to another component
or node. Each response contains references to the source and sink component or node, and its units.
The response's underlying complex data is stored in its :attr:`~.Response.complex_magnitude`
property. The magnitude and phase can be retrieved using the :attr:`~.Response.magnitude` and
:attr:`~.Response.phase` properties, respectively.
:attr:`~.Response.phase` properties, respectively. The decibel-scaled magnitude can be retrieved
using :attr:`~.Response.db_magnitude`.
.. note::
The :attr:`~.Response.magnitude` is returned with decibel (power) scaling, i.e. :math:`20 \log_{10} \left| x \right|`
where :math:`x` is the complex response. The :attr:`~.Response.phase` is returned in units of
(unwrapped) degrees.
:attr:`~.Response.db_magnitude` is returned with power scaling, i.e.
:math:`20 \log_{10} \left| x \right|` where :math:`x` is the complex response.
.. code-block:: python
......@@ -43,7 +52,7 @@ property. The magnitude and phase can be retrieved using the :attr:`~.Response.m
-8.66146537e+04+349885.52751013j, -1.95460509e+04+170108.87173014j,
-4.25456479e+03 +79773.08987768j, -9.18662496e+02 +37109.9690498j ,
-1.98014980e+02 +17233.2022651j , -4.26654531e+01 +7999.77245092j])
>>> response.magnitude
>>> response.db_magnitude
array([123.37176609, 122.86535272, 121.07307338, 116.97464649,
111.13682633, 104.67150284, 98.04946401, 91.39247246,
84.72789306, 78.06167621])
......@@ -53,16 +62,16 @@ property. The magnitude and phase can be retrieved using the :attr:`~.Response.m
90.65831778, 90.30557459])
Noise spectral densities
------------------------
~~~~~~~~~~~~~~~~~~~~~~~~
:class:`Noise spectral densities <.data.NoiseDensity>` contain the noise at a particular component
or node arising from noise produced by another component or node. They contain the :class:`noise source <.components.Noise>`
that produces the noise and a reference to the component or node that the noise is measured at, and
its units. :class:`Multi-noise spectra <.data.MultiNoiseDensity>` contain a list of multiple noise
sources; these are used to represent noise sums.
or node arising from noise produced by another component or node. They contain the :class:`noise
source <.components.Noise>` that produces the noise and a reference to the component or node that
the noise is measured at, and its units. :class:`Multi-noise spectra <.data.MultiNoiseDensity>`
contain a list of multiple noise sources; these are used to represent noise sums.
The noise spectral density's underlying data is stored in its :attr:`~.NoiseDensityBase.spectral_density`
property.
The noise spectral density's underlying data is stored in its
:attr:`~.NoiseDensityBase.spectral_density` property.
.. code-block:: python
......@@ -70,3 +79,39 @@ property.
array([1.29259971e-07, 1.00870891e-07, 8.45132667e-08, 7.57294937e-08,
7.12855936e-08, 6.91259094e-08, 6.81002020e-08, 6.76188164e-08,
6.73941734e-08, 6.72894850e-08])
Labels
~~~~~~
Functions can have labels that are used in plot legends and when :ref:`searching for functions in a
solution <solution/index:Retrieving functions>`.
Labels can be set for functions using their :attr:`~.data.BaseFunction.label` property. If no label
is set by the user, a default label is produced using the function's source and sink in the case of
single-source and -sink functions, or "Incoherent sum" for :class:`noise sums <.MultiNoiseDensity>`.
Mathematical operations
~~~~~~~~~~~~~~~~~~~~~~~
The underlying data within a function can be multiplied, divided and inverted by applying
mathematical operations to the function object. Multiplication and division can be applied using
scalars or other functions. For example, :ref:`noise spectra <data/index:Noise spectral densities>`
can be multiplied by :ref:`responses <data/index:Responses>` to project noise to a different part of
a circuit (used for example to :ref:`refer noise to the circuit input <analyses/ac/noise:Referring
noise to the input>`).
When an operation involves two functions, the units of each function are checked for
validity. As determined by the order of operation, the left function's sink must have the same units
as the right function's source. The resulting function then takes the left functions' source and the
right function's sink.
.. hint::
While the inner sources and sinks of such operations must have the same units, they do not need
to be the same :class:`element <.BaseElement>`. This is to allow functions to be lightweight and
not have to maintain a reference to the component, node or noise source objects they originally
represented (rather, just their label). It is up to the user to check that each operation makes
physical sense.
Some operations are not possible, such as multiplying noise by noise. In these cases, a
:class:`ValueError` is raised.
......@@ -16,15 +16,33 @@ Documentation style
Use `NumPy docstring format`_. Language and grammar should follow `Google style`_.
Development environment
~~~~~~~~~~~~~~~~~~~~~~~
A Visual Studio Code configuration file is provided in the project root when checked out via
``git``, which sets some code format settings which should be followed. This configuration file is
used automatically if the project is opened in Visual Studio Code from its root directory.
It may be useful to run |Zero| within a ``conda`` or ``pipenv`` environment to allow for separation
of dependencies from your system and from other projects. In both cases it is still recommended to
install |Zero| via ``pip``. For rapid development, it is highly recommended to make the project
`editable` so changes to project files reflect immediately in the library and CLI, and to install
the extra `dev` dependencies to allow you to build the documentation and run code linting tools:
.. code-block:: bash
pip install -e .[dev]
Merge requests
~~~~~~~~~~~~~~
Please open a `merge request`_ on GitLab, targeting |Zero|'s `develop` branch. To keep the git
repository's merge graph clean, ideally you should make your changes on a branch with one of the
following conventions depending on what kind of change you make:
If you have code to submit for inclusion in |Zero|, please open a `merge request`_ on GitLab
targeting the ``develop`` branch. To keep the git repository's merge graph clean, ideally you should
make your changes on a branch with one of the following conventions depending on what kind of change
you make:
- ``feature/my-feature`` for new features
- ``fix/my-fix`` for bug fixes
- ``hotfix/my-fix`` for bug fixes
Replace ``my-feature`` or ``my-fix`` with an appropriate short description. This naming scheme
roughly follows that presented in `A successful Git branching model`_.
......@@ -38,8 +56,8 @@ The steps below should be followed when creating a new release:
up-to-date.
#. Create a new release branch from ``develop``, where ``x.x.x`` is the intended new version number:
``git checkout -b release/x.x.x develop``.
#. Update default user component library ``distributed_with`` key to match the new intended version
number.
#. Update default user config and component library ``distributed_with`` keys to match the new
intended version number.
#. Commit changes and checkout ``develop``.
#. Checkout ``develop`` branch then merge release without fast-forwarding:
``git merge --no-ff release/x.x.x``.
......@@ -49,6 +67,36 @@ The steps below should be followed when creating a new release:
#. Delete the release branch: ``git branch -d release/x.x.x``.
#. Push all changes to ``master`` and ``develop`` and the new tag to origin.
Updating PyPI (pip) package
---------------------------
This requires `twine <https://packaging.python.org/key_projects/#twine>`__ and the credentials for
the |Zero| PyPI project.
#. Go to the source root directory.
#. Checkout the ``master`` branch (so the release uses the correct tag).
#. Remove previously generated distribution files:
``rm -rf build dist``
#. Create new distribution files:
``python setup.py sdist bdist_wheel``
#. (Optional) Upload distribution files to PyPI test server, entering the required credentials when
prompted:
``python -m twine upload --repository-url https://test.pypi.org/legacy/ dist/*``
You can then check the package is uploaded properly by viewing the `Zero project on the PyPI test server`_.
You can also check that it installs correctly with:
``pip install --index-url https://test.pypi.org/simple/ --no-deps zero``
Note: even if everything installs correctly, the test package will not work correctly due to lack
of dependencies (forced by the ``--no-deps`` flag, since they are not all available on the PyPI
test server).
#. Upload distribution files to PyPI, entering the required credentials when prompted:
``python -m twine upload dist/*``
#. Verify everything is up-to-date on `PyPI <https://pypi.org/project/zero/>`__.
API documentation
~~~~~~~~~~~~~~~~~
......@@ -57,9 +105,9 @@ API documentation
api/modules
.. _PEP 8: https://www.python.org/dev/peps/pep-0008/
.. _NumPy docstring format: https://numpydoc.readthedocs.io/en/latest/example.html
.. _Google style: https://developers.google.com/style/
.. _merge request: https://git.ligo.org/sean-leavey/zero/merge_requests
.. _A successful Git branching model: https://nvie.com/posts/a-successful-git-branching-model/
.. _Zero project on the PyPI test server: https://test.pypi.org/project/zero/
......@@ -80,6 +80,7 @@ Contents
components/index
analyses/index
solution/index
plotting/index
data/index
format/index
examples/index
......
.. include:: /defs.txt
Plotting
========
|Zero| relies on the powerful `Matplotlib <https://matplotlib.org/>`__ plotting library. In all
cases, the plots produced by |Zero| can be further modified in ways supported by Matplotlib.
Plotting in analysis scripts
----------------------------
In scripts, plots are generated by the call to the :ref:`solution <solution/index:Solutions>`'s
:meth:`~.Solution.plot_responses` or :meth:`~.Solution.plot_noise` methods. These support many
display options, as listed below. The return value from these methods is the
:class:`~matplotlib.figure.Figure`, which can be
:ref:`further modified <plotting/index:Further modifying plots generated by |Zero|>`.
After calling either :meth:`~.Solution.plot_responses` or :meth:`~.Solution.plot_noise`, you can
show the generated plots with :meth:`~.Solution.show`. This method is called separately to allow
you to show a number of plots simultaneously.
Responses
~~~~~~~~~
.. automethod:: zero.solution.Solution.plot_responses
:noindex:
Noise spectral densities
~~~~~~~~~~~~~~~~~~~~~~~~
.. automethod:: zero.solution.Solution.plot_noise
:noindex:
Further modifying plots generated by |Zero|
-------------------------------------------
If you wish to apply further styling to a plot generated by |Zero|, you can do so using
`Matplotlib <https://matplotlib.org/>`__ method calls on the :class:`~matplotlib.figure.Figure`
produced by or provided to the :ref:`plotting methods <plotting/index:Plotting in analysis scripts>`.
Plotting from the command line
------------------------------
The LISO compatibility command line interface provides some options for controlling the display
of plots and for saving copies to the file system. See
:ref:`LISO tools in the CLI section <cli/liso:LISO tools>` for more information.
.. include:: /defs.txt
Solutions
=========
......@@ -5,63 +7,217 @@ Solutions
>>> from zero.solution import Solution
The :class:`.Solution` class provides a mechanism for storing, displaying and saving
the output of an :ref:`analysis <analyses/index:Analyses>`; these are usually
:ref:`responses <data/index:Responses>` and :ref:`noise spectral densities <data/index:Noise spectral densities>`.
The :class:`.Solution` class provides a mechanism for storing, displaying and saving the output of
an :ref:`analysis <analyses/index:Analyses>`; these are usually :ref:`responses
<data/index:Responses>` and :ref:`noise spectral densities <data/index:Noise spectral densities>`.
Retrieving functions
--------------------
Functions can be retrieved by matching against sources, sinks, groups and (in the case of noise)
types using :meth:`.filter_responses` and :meth:`.filter_noise`. These methods return a :class:`dict`
containing the matched functions in lists keyed by their group names.
Solutions contain methods to retrieve functions contained within those solutions using a variety of
filters. The methods :meth:`.filter_responses`, :meth:`.filter_noise` and :meth:`.filter_noise_sums`
provide ways to match functions against their sources, sinks, :ref:`groups <solution/index:Groups>`
and :ref:`labels <data/index:Labels>`. These methods return a :class:`dict` containing the matched
functions in lists keyed by their group names (see :ref:`Groups <solution/index:Groups>`).
To retrieve an individual function directly, three convenience methods are available:
:meth:`.get_response`, :meth:`~.Solution.get_noise` and :meth:`~.Solution.get_noise_sum`. These take
as arguments the source, sink, group and/or label of the :class:`~.data.Response`,
:class:`~.data.NoiseDensity` or :class:`~.data.MultiNoiseDensity` to retrieve. The source and sink
in :meth:`.get_response` and the sink in :meth:`~.Solution.get_noise` and
:meth:`~.Solution.get_noise_sum` can be :class:`components <.Component>` or :class:`nodes <.Node>`
or names, while the source in :meth:`~.Solution.get_noise` can be a :class:`~.components.Noise` or
:ref:`noise specifier <solution/index:Specifying noise sources and sinks>` such as ``V(op1)``.
Sources cannot be searched against when using :meth:`~.Solution.get_noise_sum`. You can use these
convenience methods to retrieve functions when you know enough information about it to match it
amongst the solution's functions. If multiple functions are found as a result of the filters you
provide, a :class:`ValueError` is thrown.
The table below lists the available filters for the ``filter_`` methods for each function type.
With the exception of the multi-valued filters, i.e. ``sources``, ``sinks``, ``groups`` and
``labels``, these parameters are also available when using the ``get_`` methods.
=========== ============================ ========= ===== ==========
Filter Possible values Responses Noise Noise sums
=========== ============================ ========= ===== ==========
``source`` Source :class:`.Component`, ✓ ✓ ✗
:class:`.Node` or
:class:`.Noise`
``sources`` :class:`List <list>` of ✓ ✓ ✗
sources, or ``all`` for all
sources
``sink`` Sink :class:`.Component`, ✓ ✓ ✓
:class:`.Node`
``sinks`` :class:`List <list>` of ✓ ✓ ✓
sinks, or ``all`` for all
sinks
``group`` Function group name ✓ ✓ ✓
(:class:`str`)
``groups`` :class:`List <list>` of ✓ ✓ ✓
group names, or ``all`` for
all groups
``label`` Function label ✓ ✓ ✓
(:class:`str`)
``labels`` :class:`List <list>` of ✓ ✓ ✓
labels, or ``all`` for all
labels
=========== ============================ ========= ===== ==========
Specifying response sources and sinks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:class:`~.data.Response` sources and sinks (and :class:`~.components.Noise` sinks) specified in
:meth:`~.Solution.get_noise` are always components or nodes. You can specify these using either the
corresponding :class:`.Component` or :class:`.Node` objects or by specifying their name as a string.
To retrieve an individual function, two convenience methods are provided: :meth:`.get_response` and
:meth:`~.Solution.get_noise`. These take as arguments the source and sink of the :class:`~.data.Response`
or :class:`~.data.NoiseDensity` to retrieve, plus the optional group name. The source and sink in
:meth:`.get_response` and the sink in :meth:`~.Solution.get_noise` can be :class:`components <.Component>`
or :class:`nodes <.Node>` or names, while the source in :meth:`~.Solution.get_noise` can be a
:class:`~.components.Noise` or noise string such as ``V(op1)``.
Assuming that a circuit is built in the following way...
.. code-block:: python
>>> import numpy as np
>>> from zero import Circuit
>>> from zero.analysis import AcNoiesAnalysis
>>> from zero.analysis import AcSignalAnalysis
>>> circuit = Circuit()
>>> circuit.add_opamp(name="op1", model="OP27", node1="gnd", node2="nin", node3="nout")
>>> circuit.add_resistor(name="r1", value="1k", node1="nin", node2="nout")
>>> signal_analysis = AcSignalAnalysis(circuit)
>>> solution = signal_analysis.calculate(frequencies=np.logspace(0, 4, 1001), input_type="voltage", node="nin")
...responses between the input node and various nodes and components can be retrieved in the
following ways:
.. code-block:: python
>>> nin = circuit["nin"] # get the input node object
>>> nout = circuit["nout"] # get the output node object
>>> print(solution.get_response(nin, nout)) # response between input and output nodes
nin to nout (V/V)
>>> print(solution.get_response("nin", "nout")) # alternative string specifier
nin to nout (V/V)
>>> print(solution.get_response("nin", "r1")) # response between input node and resistor current (note the units)
n1 to r1 (A/V)
>>> print(solution.get_response(label="nin to r1 (A/V)")) # label specifier
n1 to r1 (A/V)
Specifying noise sources and sinks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In order to retrieve a noise function from a solution, you must specify the noise source in
:meth:`~.Solution.get_noise`. Noise sources can either be specified using their :class:`noise object
<.components.Noise>` or by building a noise specifier string. Noise sinks are specified in the same
way as response sinks (:ref:`see above <solution/index:Specifying response sources and sinks>`).
Specifying the noise source by its object involves first retrieving the component that produces the
noise. Each component holds its noise sources in its :ref:`properties <Components/index:Component
noise sources>`. For example, op-amps have voltage noise at their output and current noise at their
inverting and non-inverting inputs. Assuming the op-amp is referenced by ``op1``, these can be
retrieved using ``op1.voltage_noise``, ``op1.inv_current_noise`` and ``op1.non_inv_current_noise``,
respectively.
An alternative approach is to use a noise specifier string. These are strings constructed in the
form ``prefix(component-name[, node-name])``, with the prefix representing the type of noise as
shown in this table:
============================ ====== ==============
Noise type Prefix Example
============================ ====== ==============
Resistor (Johnson) ``R`` ``R(r1)``
Op-amp voltage ``V`` ``V(op1)``
Op-amp non-inverting current ``I`` ``I(op1, np)``
Op-amp inverting current ``I`` ``I(op1, nm)``
============================ ====== ==============
Assuming that a circuit is built in the following way...
.. code-block:: python
>>> import numpy as np
>>> from zero import Circuit
>>> from zero.analysis import AcNoiseAnalysis
>>> circuit = Circuit()
>>> circuit.add_opamp(name="op1", model="OP27", node1="gnd", node2="nin", node3="nout")
>>> circuit.add_resistor(name="r1", value="1k", node1="nin", node2="nout")
>>> op1 = circuit["op1"]
# Perform noise analysis.
>>> noise_analysis = AcNoiseAnalysis(circuit)
>>> solution = analysis.calculate(frequencies=frequencies, input_type="voltage", node="nin", sink="nout")
# Get voltage noise from op-amp at the output node.
>>> str(solution.get_noise(op1.voltage_noise, "nout"))
'V(op1) to nout'
# Alternatively retrieve using string.
>>> str(solution.get_noise("V(op1)", "nout"))
'V(op1) to nout'
>>> solution = noise_analysis.calculate(frequencies=np.logspace(0, 4, 1001), input_type="voltage", node="nin", sink="nout")
...noise functions can be retrieved with e.g.:
.. code-block:: python
>>> op1 = circuit["op1"] # get the op1 object
>>> print(solution.get_noise(op1.voltage_noise, "nout")) # voltage noise at op1
V(op1) to nout
>>> print(solution.get_noise("V(op1)", "nout")) # alternative string specifier
V(op1) to nout
>>> print(solution.get_noise(op1.inv_current_noise, "nout")) # current noise at op1's inverting input
I(op1, nin) to nout
>>> print(solution.get_noise("I(op1, nin)", "nout")) # alternative string specifier
I(op1, nin) to nout
>>> print(solution.get_noise(label="I(op1, nin) to nout")) # label specifier