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

Merge branch 'develop' into feature/input-noise

parents 50f9ab78 d2f079d8
......@@ -3,7 +3,6 @@
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache"
LISO_GIT_ARCHIVE_URL: "https://gitlab.aei.uni-hannover.de/api/v4/projects/143/repository/archive?private_token=${LISO_ACCESS_TOKEN}"
cache:
paths:
......@@ -30,6 +29,10 @@ stages:
- make test-integration
.template:test:validation: &template-test-validation
only:
variables:
- $LISO_GIT_ARCHIVE_ACCESS_TOKEN
- $LISO_GIT_ARCHIVE_URL
script:
- mkdir liso
- cd liso
......@@ -45,27 +48,6 @@ stages:
- pip install .
- make test-validation
.template:deploy:docs: &template-deploy-docs
script:
- mkdir liso
- cd liso
- wget -O liso.tar.gz ${LISO_GIT_ARCHIVE_URL}
- tar -xvf liso.tar.gz -C . --wildcards liso-linux*/ --strip-components=1
- 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
- pip install .[dev]
- cd docs
- make html
- mv _build/html ../public
artifacts:
paths:
- public
test unit:latest:
image: python:latest
stage: test
......@@ -112,9 +94,49 @@ test validation:3.7:
<<: *template-test-validation
# Generate the documentation only on creation of new tags.
pages:
deploy pages:
image: python:3.7
stage: deploy
only:
refs:
- tags
variables:
- $LISO_GIT_ARCHIVE_ACCESS_TOKEN
- $LISO_GIT_ARCHIVE_URL
script:
- mkdir liso
- cd liso
- wget -O liso.tar.gz ${LISO_GIT_ARCHIVE_URL}
- tar -xvf liso.tar.gz -C . --wildcards liso-linux*/ --strip-components=1
- 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
- pip install .[dev]
- cd docs
- make html
- mv _build/html ../public
artifacts:
paths:
- public
# Generate PyPI release only on creation of new tags.
deploy pypi:
image: python:3.7
stage: deploy
only:
- tags
<<: *template-deploy-docs
refs:
- tags
variables:
- $TWINE_USERNAME
- $TWINE_PASSWORD
script:
- python --version
- apt update -qy
- apt install --assume-yes python-pip
- pip install twine
- python setup.py sdist bdist_wheel
- python -m twine upload dist/zero-*
......@@ -6,5 +6,9 @@
"editor.insertSpaces": true,
"[markdown]": {
"files.trimTrailingWhitespace": false
}
},
"restructuredtext.linter.extraArgs": [
"--max-line-length 100",
],
"restructuredtext.confPath": "${workspaceFolder}/docs",
}
......@@ -20,7 +20,8 @@ STATICDIR = _static
# Static files
ZEROSTATICDEPS = $(STATICDIR)/liso-input-node-graph.svg
# Zero Python files.
ZEROPYTHONDEPS = $(STATICDIR)/liso-input-response.svg $(STATICDIR)/solution-combination.svg
ZEROPYTHONDEPS = $(STATICDIR)/liso-input-response.svg $(STATICDIR)/solution-combination.svg \
$(STATICDIR)/resistor-current-noise.svg
# LISO script comparisons.
ZEROLISODEPS = $(STATICDIR)/liso-compare-response.svg
......
"""Documentation example. Requires target figure filename as argument."""
import sys
import numpy as np
from zero import Circuit
from zero.analysis import AcNoiseAnalysis
from zero.noise import VoltageNoise
# Create a new noise type.
class ResistorCurrentNoise(VoltageNoise):
"""Resistor current noise source.
This models resistor current noise. See e.g. https://dcc.ligo.org/LIGO-T0900200/public
for more details. This noise depends on resistor composition and on its current. Be
careful when using this noise - it generally does not transfer to different circuits
with identical resistors as it depends on the voltage drop across the resistor.
Parameters
----------
vnoise : :class:`float`
The voltage noise at the specified frequency (V/sqrt(Hz)).
frequency : :class:`float`
The frequency at which the specified voltage noise is defined (Hz).
exponent : :class:`float`
The frequency exponent to use for calculating the frequency response.
"""
def __init__(self, vnoise, frequency=1.0, exponent=0.5, **kwargs):
super().__init__(**kwargs)
self.vnoise = vnoise
self.frequency = frequency
self.exponent = exponent
def noise_voltage(self, frequencies, **kwargs):
return self.vnoise * self.frequency / frequencies ** self.exponent
@property
def label(self):
return f"RE({self.component.name})"
# 1000 frequencies between 0.1 Hz to 10 kHz
frequencies = np.logspace(-1, 4, 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", name="r1")
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")
# Add resistor current noise to r1 with 10 nV/sqrt(Hz) at 1 Hz, with 1/f^2 drop-off.
r1 = circuit["r1"]
r1.add_noise(ResistorCurrentNoise(vnoise=1e-8, frequency=1.0, exponent=0.5))
# Solve circuit.
analysis = AcNoiseAnalysis(circuit=circuit)
solution = analysis.calculate(frequencies=frequencies, input_type="voltage", node="n1",
sink="nout", incoherent_sum=True)
# Plot.
plot = solution.plot_noise(sink="nout")
solution.save_figure(plot, sys.argv[1])
......@@ -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
---------------------
......@@ -16,9 +20,10 @@ the specified noise sink being calculated and added as a single function to the
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.
: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``
......@@ -52,3 +57,24 @@ Add incoherent sums of all resistor and op-amp noise:
# 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.
......@@ -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
-----------------
......
......@@ -14,6 +14,7 @@ Components
passive-components
op-amps
noise
What is a 'component'?
----------------------
......@@ -91,14 +92,6 @@ Inductor l l1
Op-amp op op1
========= ====== =======
Component noise sources
-----------------------
Some components directly produce noise at a node they are connected to (:class:`.NodeNoise`).
Others create noise affecting current flow (:class:`.ComponentNoise`). The type and amount
of noise depends on the component; for example, :class:`capacitors <.Capacitor>` do not
produce noise, whereas :class:`resistors <.Resistor>` do (:class:`Johnson noise <.JohnsonNoise>`).
Setting a component's value
---------------------------
......
.. include:: /defs.txt
.. currentmodule:: zero.components
Noise
=====
Some components in |Zero| produce noise, such as resistors (:ref:`components/noise:Johnson noise`)
and op-amps (:ref:`voltage <components/noise:Op-amp voltage noise>` and :ref:`current
<components/noise:Op-amp current noise>` noise). Other components such as :ref:`capacitors
<components/passive-components:Capacitors>` and :ref:`inductors
<components/passive-components:Inductors>` do not produce noise by default, although noise can be
:ref:`added by the user <components/noise:Defining new noise sources>`.
Johnson noise
-------------
`Johnson noise <https://en.wikipedia.org/wiki/Johnson%E2%80%93Nyquist_noise>`__ is a type of
voltage noise in resistors that arises from thermal agitation of charge carriers. This is a function
of temperature but has no dependence on applied voltage or current.
The default temperature assumed in |Zero| calculations is set in the :ref:`configuration
<configuration/index:Configuration>`.
Op-amp noise
------------
Op-amps produce voltage noise across their input and output nodes, and current noise is present at
their input nodes.
Op-amp voltage noise
~~~~~~~~~~~~~~~~~~~~
Op-amps produce voltage noise across their input and output nodes. The noise is a function of
frequency, usually with a flat component at all frequencies and a component rising towards low
frequencies. The cross-over between these two noise components is typically around 1 to 100 Hz,
though this varies depending on the type of op-amp. BJT-based op-amps typically have the lowest
voltage noise.
Op-amp current noise
~~~~~~~~~~~~~~~~~~~~
Current noise is present at op-amps' inputs. The noise is a function of frequency, usually with a
flat component at all frequencies and a component rising towards low frequencies. The cross-over
between these two noise components is typically around 100 Hz to 1 kHz, though this varies depending
on the type of op-amp. FET-based op-amps typically have the lowest current noise.
Current noise is converted to voltage noise by resistors connected to the op-amp inputs. That means
that in a standard op-amp circuit with a feedback resistor, the current noise scales with the
feedback resistance.
In |Zero|, current noise is considered identical for both input nodes. This is usually a valid
assumption for voltage-feedback op-amps, which are the type that |Zero| models.
Defining new noise sources
--------------------------
New noise sources can be defined in |Zero| and added to components. The noise will then appear in
:ref:`noise analyses <analyses/ac/noise:Small AC noise analysis>`.
Noise sources can be created by subclassing one of the available noise types: :class:`.VoltageNoise`
or :class:`.CurrentNoise`. The implementation must define a ``label`` property and set a method to
call when computing the noise. This method will receive the current frequency vector and it must
return the corresponding noise.
Here is an example of defining a resistor current noise source and using it in a circuit:
.. code-block:: python
import numpy as np
from zero import Circuit
from zero.analysis import AcNoiseAnalysis
from zero.noise import VoltageNoise
# Create a new noise type.
class ResistorCurrentNoise(VoltageNoise):
"""Resistor current noise source.
This models resistor current noise. See e.g. https://dcc.ligo.org/LIGO-T0900200/public
for more details. This noise depends on resistor composition and on its current. Be
careful when using this noise - it generally does not transfer to different circuits
with identical resistors as it depends on the voltage drop across the resistor.
Parameters
----------
vnoise : :class:`float`
The voltage noise at the specified frequency (V/sqrt(Hz)).
frequency : :class:`float`
The frequency at which the specified voltage noise is defined (Hz).
exponent : :class:`float`
The frequency exponent to use for calculating the frequency response.
"""
def __init__(self, vnoise, frequency=1.0, exponent=0.5, **kwargs):
super().__init__(**kwargs)
self.vnoise = vnoise
self.frequency = frequency
self.exponent = exponent
def noise_voltage(self, frequencies, **kwargs):
return self.vnoise * self.frequency / frequencies ** self.exponent
@property
def label(self):
return f"RE({self.component.name})"
# 1000 frequencies between 0.1 Hz to 10 kHz
frequencies = np.logspace(-1, 4, 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", name="r1")
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")
# Add resistor current noise to r1 with 10 nV/sqrt(Hz) at 1 Hz, with 1/f^2 drop-off.
r1 = circuit["r1"]
r1.add_noise(ResistorCurrentNoise(vnoise=1e-8, frequency=1.0, exponent=0.5))
# Solve circuit.
analysis = AcNoiseAnalysis(circuit=circuit)
solution = analysis.calculate(frequencies=frequencies, input_type="voltage", node="n1",
sink="nout", incoherent_sum=True)
# Plot.
solution.plot_noise(sink="nout")
solution.show()
.. image:: /_static/resistor-current-noise.svg
......@@ -25,10 +25,8 @@ the voltage gain is modified.
Noise
=====
Op-amps produce :class:`voltage noise <VoltageNoise>` across their input
and output :class:`nodes <Node>`, and :class:`current noise <CurrentNoise>`
is present at its input :class:`nodes <Node>`. The noise is a function of
frequency.
Op-amps produce voltage noise across their input and output nodes, and current noise is present at
their input nodes. See :ref:`components/noise:Op-amp noise` for more details.
Library
=======
......@@ -30,7 +30,7 @@ The resistance can be changed using the resistor's :meth:`~Resistor.resistance`
>>> r.resistance = "1.1M"
In a circuit, resistor produce :class:`Johnson noise <.JohnsonNoise>`.
In a circuit, resistor produce :class:`Johnson noise <.ResistorJohnsonNoise>`.
Capacitors
==========
......
......@@ -8,20 +8,29 @@ 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.
......@@ -53,16 +62,16 @@ using :attr:`~.Response.db_magnitude`.
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