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

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.
This diff is collapsed.
"""Native circuit construction and simulation
This simulates a simple non-inverting whitening filter's responses for a voltage input.
"""A simple non-inverting whitening filter's response to a voltage input.
https://www.circuitlab.com/circuit/62vd4a/whitening-non-inverting/
......
"""Native circuit construction and simulation
This simulates a simple non-inverting whitening filter's responses for a current input.
"""A simple non-inverting whitening filter's response to a current input, with scaling applied.
https://www.circuitlab.com/circuit/62vd4a/whitening-non-inverting/
......@@ -29,6 +27,13 @@ if __name__ == "__main__":
analysis = AcSignalAnalysis(circuit=circuit)
solution = analysis.calculate(frequencies=frequencies, input_type="current", node="n1")
# Plot.
solution.plot_responses(sinks=["nout"])
# Scale transfer functions by 2 (e.g. to account for 50Ω network analyser as discussed in
# footnote 2 on p16 of the LISO manual v1.61).
solution.scale_responses(2)
# Add a response reference curve.
solution.add_response_reference(frequencies, np.ones_like(frequencies), label="Unity gain")
# Plot, scaling in absolute units.
solution.plot_responses(sink="nout", scale_db=False)
solution.show()
"""Native circuit construction and simulation
This simulates a simple non-inverting whitening filter's output noise.
"""A simple non-inverting whitening filter's output noise.
https://www.circuitlab.com/circuit/62vd4a/whitening-non-inverting/
......@@ -28,8 +26,12 @@ if __name__ == "__main__":
# Solve circuit.
analysis = AcNoiseAnalysis(circuit=circuit)
solution = analysis.calculate(frequencies=frequencies, input_type="voltage", node="n1",
sink="nout")
sink="nout", incoherent_sum=True)
# Give the sum a different label.
noise_sum = solution.get_noise_sum(sink="nout")
noise_sum.label = "Total noise"
# Plot.
solution.plot_noise(sinks=["nout"])
solution.plot_noise(sink="nout")
solution.show()
"""A simple non-inverting whitening filter's output noise, plotted with various incoherent sums.
https://www.circuitlab.com/circuit/62vd4a/whitening-non-inverting/
Sean Leavey
"""
import numpy as np
from zero import Circuit
from zero.analysis import AcNoiseAnalysis
if __name__ == "__main__":
# 1000 frequencies between 1 Hz to 1 MHz
frequencies = np.logspace(0, 6, 1000)
# Create circuit object.
circuit = Circuit()
# Add components.
circuit.add_capacitor(value="10u", node1="gnd", node2="n1")
circuit.add_resistor(value="430", node1="n1", node2="nm")
circuit.add_resistor(value="43k", node1="nm", node2="nout")
circuit.add_capacitor(value="47p", node1="nm", node2="nout")
circuit.add_library_opamp(model="LT1124", node1="gnd", node2="nm", node3="nout")
# Solve circuit.
analysis = AcNoiseAnalysis(circuit=circuit)
solution = analysis.calculate(frequencies=frequencies, input_type="voltage", node="n1",
sink="nout", incoherent_sum={"total": "all",
"resistors": "allr",
"op-amps": "allop"})
# Plot.
solution.plot_noise(sink="nout")
solution.show()
"""Photodetector readout circuit noise, projected to the circuit's input.
Rana Adhikari, Sean Leavey
"""
import numpy as np
from scipy.constants import elementary_charge
from zero import Circuit
from zero.analysis import AcNoiseAnalysis
def shot_noise(vector, photocurrent):
"""Shot noise current."""
return np.ones_like(vector) * np.sqrt(2 * elementary_charge * photocurrent)
if __name__ == "__main__":
# 1000 frequencies between 0.1 Hz to 100 kHz
frequencies = np.logspace(-1, 5, 1000)
# Create circuit object.
circuit = Circuit()
# The photodiode is a current source that connects through a photodiode circuit model (shunt
# capacitor and series resistor).
circuit.add_capacitor(value="200p", node1="gnd", node2="nD")
circuit.add_resistor(value="10", node1="nD", node2="nm")
# Transimpedance amplifier.
circuit.add_library_opamp(model="OP27", node1="gnd", node2="nm", node3="nout")
circuit.add_resistor(value="1k", node1="nm", node2="nout")
# Solve circuit.
analysis = AcNoiseAnalysis(circuit=circuit)
solution = analysis.calculate(frequencies=frequencies, input_type="current", node="nD",
sink="nout", impedance="1G", incoherent_sum=True,
input_refer=True)
# Add a shot noise reference curve.
solution.add_noise_reference(frequencies, shot_noise(frequencies, 1e-3), label="Shot noise")
# Plot. Note that the sink is now the input, since we projected the noise there.
solution.plot_noise(sink="input", ylim=(1e-14, 1e-9), title="Photodetector noise")
solution.show()
"""Photodetector readout circuit noise, projected to the circuit's input, projected into
displacement noise.
Rana Adhikari, Sean Leavey
"""
import numpy as np
from zero import Circuit
from zero.analysis import AcNoiseAnalysis
from zero.tools import create_response
if __name__ == "__main__":
# 1000 frequencies between 0.1 Hz to 100 kHz
frequencies = np.logspace(-1, 5, 1000)
# Create circuit object.
circuit = Circuit()
# The photodiode is a current source that connects through a photodiode circuit model (shunt
# capacitor and series resistor).
circuit.add_capacitor(value="200p", node1="gnd", node2="nD")
circuit.add_resistor(value="10", node1="nD", node2="nm")
# Transimpedance amplifier.
circuit.add_library_opamp(model="OP27", node1="gnd", node2="nm", node3="nout")
circuit.add_resistor(value="1k", node1="nm", node2="nout")
# Solve circuit.
analysis = AcNoiseAnalysis(circuit=circuit)
solution = analysis.calculate(frequencies=frequencies, input_type="current", node="nD",
sink="nout", impedance="1G", incoherent_sum=True,
input_refer=True)
# Scale all noise at the input to displacement.
pd_to_displacement = create_response(source="input", sink="displacement", source_unit="A",
sink_unit="m", data=1e-9*np.ones_like(frequencies),
frequencies=frequencies)
solution.scale_noise(pd_to_displacement, sink="input")
# Plot. Note that the sink is now the input, since we projected the noise there.
solution.plot_noise(sink="displacement", ylim=(1e-23, 1e-18), title="Photodetector noise")
solution.show()
"""Circuit node graph display.
Sean Leavey
"""
from zero import Circuit
from zero.display import NodeGraph
if __name__ == "__main__":
# Create circuit object.
circuit = Circuit()
# Add components.
circuit.add_capacitor(value="10u", node1="gnd", node2="n1")
circuit.add_resistor(value="430", node1="n1", node2="nm")