Commit 929071d2 authored by Sean Leavey's avatar Sean Leavey
Browse files

Update docs to automatically generate some plots; add more examples

parent 200ee17c
......@@ -19,9 +19,6 @@ STATICDIR = _static
### Dynamic plot dependencies
# Static files
ZEROSTATICDEPS = $(STATICDIR)/liso-input-node-graph.svg
# Zero Python files.
ZEROPYTHONDEPS = $(STATICDIR)/liso-input-response.svg $(STATICDIR)/solution-combination.svg \
$(STATICDIR)/resistor-current-noise.svg
# Native plots with LISO files.
ZEROLISODEPS = $(STATICDIR)/liso-two-noises.svg
# Native/LISO script comparisons.
......@@ -83,11 +80,6 @@ $(CLIOPAMPGAINDEP):
@echo "Generating $@"
zero library search "gbw > 800M & ((vnoise < 10n & inoise < 10p) | (vnoise < 100n & inoise < 1p)) & model != OP00" --no-plot-gain --save-gain-figure $@ --fstop 1M
# Generate SVG plots of Zero output using Python script.
$(STATICDIR)/%.svg: $(STATICDIR)/zero-python/%.py
@echo "Generating $@ from $<"
python $< $@
# Generate SVG plot of two noise example.
$(STATICDIR)/liso-two-noises.svg:
@echo "Generating $@"
......
"""Documentation example. Requires target figure filename as argument."""
import sys
from zero.liso import LisoInputParser
parser = LisoInputParser()
parser.parse("""
c c1 10u gnd n1
r r1 430 n1 nm
r r2 43k nm nout
c c2 47p nm nout
op o1 lt1124 nin nm nout
freq log 1 100k 100
uinput nin 0
uoutput nout:db:deg
""")
solution = parser.solution()
plotter = solution.plot_responses()
plotter.save(sys.argv[1])
"""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.
plotter = solution.plot_noise(sink="nout")
plotter.save(sys.argv[1])
"""Documentation example. Requires target figure filename as argument."""
import sys
from zero.liso import LisoInputParser
# Create parser.
parser = LisoInputParser()
base_circuit = """
l l2 420n nlf nout
c c4 47p nlf nout
c c1 1n nrf gnd
r r1 1k nrf gnd
l l1 600n nrf n_l1_c2
c c2 330p n_l1_c2 n_c2_c3
c c3 33p n_c2_c3 nout
c load 20p nout gnd
freq log 100k 100M 1000
uoutput nout
"""
# Parse the base circuit.
parser.parse(base_circuit)
# Set the circuit input to the low frequency port.
parser.parse("uinput nlf 50")
# Ground the unused input.
parser.parse("r nrfsrc 5 nrf gnd")
# Calculate the solution.
solutionlf = parser.solution()
solutionlf.name = "LF Circuit"
# Reset the parser's state.
parser.reset()
# Parse the base circuit.
parser.parse(base_circuit)
# Set the input to the radio frequency port.
parser.parse("uinput nrf 50")
# Ground the unused input.
parser.parse("r nlfsrc 5 nlf gnd")
# Calculate the solution.
solutionrf = parser.solution()
solutionrf.name = "RF Circuit"
# 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.
plotter = solution.plot_responses()
plotter.save(sys.argv[1])
......@@ -65,7 +65,8 @@ return the corresponding noise.
Here is an example of defining a resistor current noise source and using it in a circuit:
.. code-block:: python
.. plot::
:include-source:
import numpy as np
from zero import Circuit
......@@ -129,6 +130,3 @@ Here is an example of defining a resistor current noise source and using it in a
# Plot.
solution.plot_noise(sink="nout")
solution.show()
.. image:: /_static/resistor-current-noise.svg
......@@ -43,7 +43,8 @@ extensions = [
'sphinx.ext.coverage',
'sphinx.ext.viewcode',
'sphinx.ext.napoleon',
'sphinx_click.ext'
'sphinx_click.ext',
'matplotlib.sphinxext.plot_directive',
]
# Add any paths that contain templates here, relative to this directory.
......@@ -378,3 +379,11 @@ napoleon_use_param = False
# show return type on same line as description
napoleon_use_rtype = False
# -- Options for Matplotlib plot directive --------------------------------
# Code to run before plot scripts.
plot_pre_code = None
# Plot formats to show under each generated plot.
plot_formats = [('png', 200), 'pdf']
......@@ -3,34 +3,282 @@
Examples
========
Basic circuits
--------------
This page lists some example circuits in |Zero|. Some similar circuit examples are provided in the
`development repository`_.
Some basic circuit example scripts are provided in the |Zero| source directory, or in the
`development repository`_. These scripts can be run directly as long as |Zero| is installed and
available.
Non-inverting whitening filter response
---------------------------------------
LISO
----
The following is the voltage-voltage response from a non-inverting whitening filter, providing
34 dB gain between around 10 Hz to 1 kHz.
.. toctree::
:maxdepth: 2
.. plot::
:include-source:
liso-input
import numpy as np
from zero import Circuit
from zero.analysis import AcSignalAnalysis
.. _development repository: https://git.ligo.org/sean-leavey/zero/tree/master/examples/native
# 1000 frequencies between 10 mHz to 100 kHz.
frequencies = np.logspace(-2, 5, 1000)
# Create circuit object.
circuit = Circuit()
# Add components.
circuit.add_library_opamp(model="LT1124", node1="n1", node2="n3", node3="n4")
circuit.add_capacitor(value="10u", node1="gnd", node2="n2")
circuit.add_resistor(value="2k", node1="n2", node2="n3")
circuit.add_resistor(value="100k", node1="n3", node2="n4")
circuit.add_capacitor(value="1.5n", node1="n3", node2="n4")
# Solve circuit.
analysis = AcSignalAnalysis(circuit=circuit)
solution = analysis.calculate(frequencies=frequencies, input_type="voltage", node="n1")
# Plot.
plot = solution.plot_responses(sink="n4")
plot.show()
Inverting whitening filter response
-----------------------------------
The following is the voltage-voltage response from an inverting whitening filter, providing 34 dB
gain between around 10 Hz to 1 kHz. The response is almost identical to the `non-inverting
filter above <#non-inverting-whitening-filter-response>`__ except its output has opposite sign (and
the input impedance is much lower).
.. plot::
:include-source:
import numpy as np
from zero import Circuit
from zero.analysis import AcSignalAnalysis
# 1000 frequencies between 10 mHz to 100 kHz.
frequencies = np.logspace(-2, 5, 1000)
# Create circuit object.
circuit = Circuit()
# Add components.
circuit.add_capacitor(value="10u", node1="n1", node2="n2")
circuit.add_resistor(value="2k", node1="n2", node2="n3")
circuit.add_resistor(value="100k", node1="n1", node2="n3")
circuit.add_library_opamp(model="LT1124", node1="gnd", node2="n3", node3="n4")
circuit.add_resistor(value="100k", node1="n3", node2="n4")
circuit.add_capacitor(value="1.5n", node1="n3", node2="n4")
# Solve circuit.
analysis = AcSignalAnalysis(circuit=circuit)
solution = analysis.calculate(frequencies=frequencies, input_type="voltage", node="n1")
# Plot.
plot = solution.plot_responses(sink="n4")
plot.show()
Inverting whitening filter output noise
---------------------------------------
The voltage noise at the output node (``nout``) can be calculated with the code below.
.. plot::
:include-source:
import numpy as np
from zero import Circuit
from zero.analysis import AcNoiseAnalysis
# 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", 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")
# 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")
plot.show()
The current noise through the op-amp can be calculated with the code below. Note that it is useful
to give the op-amp an explicit name in order to reference it as the noise sink.
.. plot::
:include-source:
import numpy as np
from zero import Circuit
from zero.analysis import AcNoiseAnalysis
# 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", name="r1")
circuit.add_resistor(value="43k", node1="nm", node2="nout")
circuit.add_capacitor(value="47p", node1="nm", node2="nout")
circuit.add_library_opamp(name="op1", 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="op1", incoherent_sum=True)
# Plot.
plot = solution.plot_noise(sink="op1")
plot.show()
Input-projected photodetector readout noise
-------------------------------------------
With photodetectors used in optics experiments, it is often very useful to present the readout noise
from the electronics in terms of the equivalent input noise. Photodiodes used at the input of these
readout circuits can usually be considered as current sources, so projecting the noise to the input
involves projecting into units of ``A/sqrt(Hz)``. It is often yet more useful to scale this input
noise some other unit, such as displacement (useful for instance in interferometer experiments).
The example here shows the projection of the total output voltage noise to the equivalent input
current noise and the scaling of the input current noise to displacement noise.
.. plot::
:include-source:
import numpy as np
from zero import Circuit
from zero.analysis import AcNoiseAnalysis
from zero.tools import create_response
# 1000 frequencies between 10 mHz to 1 kHz
frequencies = np.logspace(-2, 3, 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. Assume an input impedance of 1 GΩ for the photodiode.
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 (response assumed to be 1 nm/A).
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. We also set a
# y-limit so the plot doesn't expand to show the (very small) resistor noise.
plot = solution.plot_noise(sink="displacement", title="Photodetector noise",
ylim=(1e-22, 1e-19))
plot.show()
LISO scripts
------------
Input file parsing
~~~~~~~~~~~~~~~~~~
|Zero| is capable of parsing :ref:`most <liso/input:Known incompatibilities>` LISO input files.
To start, create a new parser:
.. plot::
:include-source:
:context:
:nofigs:
from zero.liso import LisoInputParser
parser = LisoInputParser()
To parse a LISO circuit, either call the :meth:`~.LisoParser.parse` method with text:
.. plot::
:include-source:
:context:
:nofigs:
parser.parse("""
c c1 10u gnd n1
r r1 430 n1 nm
r r2 43k nm nout
c c2 47p nm nout
op op1 lt1124 nin nm nout
freq log 1 100k 100
uinput nin 0
uoutput nout:db:deg
""")
Or point it to a file using the :code:`path` parameter:
.. code-block:: python
parser.parse(path="/path/to/liso/script.fil")
Get the solution with :meth:`~.LisoParser.solution` and plot and show it with
:meth:`.Solution.plot` and :meth:`.Solution.show`:
.. plot::
:include-source:
:context:
solution = parser.solution()
solution.plot()
solution.show()
You can at any time list the circuit's constituent components with :code:`print(parser.circuit)`:
.. code-block:: console
Circuit with 6 components and 5 nodes
1. c1 [in=gnd, out=n1, C=1e-05]
2. c2 [in=nm, out=nout, C=4.7e-11]
3. input [in=gnd, out=nin, Z=default]
4. op1 [in+=nin, in-=nm, out=nout, model=LT1124]
5. r1 [in=n1, out=nm, R=430.0]
6. r2 [in=nm, out=nout, R=43000.0]
Generating a circuit graph
--------------------------
You can plot the circuit's node network using Graphviz, if installed:
You can plot the circuit's node network using Graphviz, if installed. Using the circuit from the
`above example <#inverting-whitening-filter>`__, the following code will generate and open a PDF of
the circuit's node network:
.. code:: python
>>> from zero.display import NodeGraph
>>> NodeGraph(circuit)
from zero.display import NodeGraph
graph = NodeGraph(circuit)
graph.view_pdf()
.. image:: /_static/liso-input-node-graph.svg
Graphviz almost always produces a messy representation, but it can sometimes be useful to help
spot simple mistakes in circuit definitions.
.. _development repository: https://git.ligo.org/sean-leavey/zero/tree/master/examples/native
.. include:: /defs.txt
Input file parsing
==================
.. code:: python
>>> from zero.liso import LisoInputParser
|Zero| is capable of parsing :ref:`most <liso/input:Known incompatibilities>` LISO input files.
To start, create a new parser:
.. code:: python
>>> parser = LisoInputParser()
To parse a LISO circuit, either call the :meth:`~.LisoParser.parse` method with text:
.. code:: python
>>> parser.parse("""
c c1 10u gnd n1
r r1 430 n1 nm
r r2 43k nm nout
c c2 47p nm nout
op o1 lt1124 nin nm nout
freq log 1 100k 100
uinput nin 0
uoutput nout:db:deg
""")
Or point it to a file using the :code:`path` parameter:
.. code:: python
>>> parser.parse(path="/path/to/liso/script.fil")
Get the solution with :meth:`~.LisoParser.solution` and plot and show it with
:meth:`.Solution.plot` and :meth:`.Solution.show`:
.. code:: python
>>> solution = parser.solution()
>>> solution.plot()
>>> solution.show()
.. image:: /_static/liso-input-response.svg
You can at any time list the circuit's constituent components:
.. code-block:: python
>>> parser.circuit
Circuit with 6 components and 5 nodes
1. c1 [in=gnd, out=n1, C=1e-05]
2. c2 [in=nm, out=nout, C=4.7e-11]
3. input [in=gnd, out=nin, Z=default]
4. o1 [in+=nin, in-=nm, out=nout, model=LT1124]
5. r1 [in=n1, out=nm, R=430.0]
6. r2 [in=nm, out=nout, R=43000.0]
......@@ -212,7 +212,8 @@ The resulting solution's group names can be changed using :meth:`.rename_group`.
Here is an example of solution combination using a :ref:`LISO model <liso/index:LISO compatibility>`
of an RF summing box with two inputs and one output:
.. code-block:: python
.. plot::
:include-source:
from zero.liso import LisoInputParser
......@@ -265,8 +266,6 @@ of an RF summing box with two inputs and one output:
solution.plot()
solution.show()
.. image:: /_static/solution-combination.svg
.. hint::
The above example makes a call to :meth:`~.Solution.plot`. This relies on :ref:`default
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment