├── __init__.py ├── docs_requirements.txt ├── test_requirements.txt ├── .gitignore ├── broadbean ├── __init__.py ├── broadbean.py ├── ripasso.py ├── tools.py ├── plotting.py ├── element.py ├── blueprint.py └── sequence.py ├── docs ├── execute_notebooks.cmd ├── source │ ├── index.rst │ └── conf.py └── Makefile ├── .travis.yml ├── .appveyor.yml ├── LICENSE ├── setup.py ├── README.md └── tests ├── test_awgfilegeneration.py ├── test_subsequences.py ├── test_forging.py ├── test_element.py ├── test_sequence.py └── test_blueprint.py /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs_requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx_rtd_theme 3 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | hypothesis 2 | pytest 3 | mypy 4 | jupyter 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | 3 | # Sphinx documentation 4 | docs/build/ 5 | -------------------------------------------------------------------------------- /broadbean/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa (ignore unused imports) 2 | 3 | # Version 1.0 4 | 5 | from . import ripasso 6 | from .element import Element 7 | from .sequence import Sequence 8 | from .blueprint import BluePrint 9 | from .tools import makeVaryingSequence, repeatAndVarySequence 10 | from .broadbean import PulseAtoms 11 | -------------------------------------------------------------------------------- /docs/execute_notebooks.cmd: -------------------------------------------------------------------------------- 1 | echo "Running the notebooks..." 2 | jupyter nbconvert --to notebook --execute "Pulse Building Tutorial.ipynb" 3 | jupyter nbconvert --to notebook --execute "Filter compensation.ipynb" 4 | jupyter nbconvert --to notebook --execute "Subsequences.ipynb" 5 | echo "Cleaning up the generated output..." 6 | rm *nbconvert.ipynb 7 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. broadbean documentation master file, created by 2 | sphinx-quickstart on Wed May 17 09:11:27 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to broadbean's documentation! 7 | 8 | The broadbean package is a tool for creating and manipulating pulse 9 | sequences. 10 | 11 | ===================================== 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | :caption: Contents: 16 | 17 | .. automodule:: broadbean.broadbean 18 | :members: 19 | 20 | .. automodule:: broadbean.ripasso 21 | :members: 22 | 23 | Indices and tables 24 | ================== 25 | 26 | * :ref:`genindex` 27 | * :ref:`modindex` 28 | * :ref:`search` 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # travis file for the broadbean package 2 | 3 | language: python 4 | python: 5 | - "3.6" 6 | 7 | # be a good guy and potentially save time 8 | cache: pip 9 | 10 | # Don't receive notifications for now 11 | notifications: 12 | email: false 13 | 14 | branches: 15 | only: 16 | - master 17 | - version1.0 18 | 19 | install: 20 | - pip install --upgrade pip 21 | - pip install -r test_requirements.txt 22 | - pip install . 23 | 24 | # command sequence to run tests, including executing the 25 | # tutorial notebooks 26 | script: 27 | - mypy broadbean --ignore-missing-imports 28 | - cd tests 29 | - pytest 30 | - cd ../docs 31 | - make execute 32 | 33 | after_success: 34 | - cd .. 35 | - pip install -r docs_requirements.txt 36 | - make -f docs/Makefile gh-pages 37 | -------------------------------------------------------------------------------- /.appveyor.yml: -------------------------------------------------------------------------------- 1 | # AppVeyor configuration file for broadbean 2 | 3 | branches: 4 | only: 5 | - master 6 | - version1.0 7 | 8 | environment: 9 | global: 10 | CONDA_INSTALL_LOCATION: "C:\\Miniconda36-x64" 11 | 12 | # Do not use MSBuild 13 | build: false 14 | 15 | # Init scripts 16 | 17 | # Install scripts 18 | install: 19 | - set PATH=%CONDA_INSTALL_LOCATION%;%CONDA_INSTALL_LOCATION%\scripts;%PATH%; 20 | - conda config --set always_yes true # else AppVeyor will hang forever waiting for user ([y]/n) 21 | - conda update -n base conda 22 | - conda info -a 23 | - conda create -q -n test-environment python=%PYTHON_VERSION% pip setuptools 24 | - activate test-environment 25 | - python -m pip install --upgrade pip 26 | - pip install -r test_requirements.txt 27 | # temporary solution while waiting for all files for matplotlib 3.0.0 on pypi 28 | - pip install matplotlib==2.2.3 29 | - pip install . 30 | 31 | # Test scripts 32 | test_script: 33 | - mypy broadbean --ignore-missing-imports 34 | - cd tests 35 | - pytest 36 | - cd ../docs 37 | - execute_notebooks.cmd 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 QCoDeS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='broadbean', 5 | version='0.9.1', 6 | 7 | # We might as well require what we know will work 8 | # although older numpy and matplotlib version will probably work too 9 | install_requires=['numpy>=1.12.1', 10 | 'matplotlib', 11 | 'schema'], 12 | 13 | author='William H.P. Nielsen', 14 | author_email='William.Nielsen@microsoft.com', 15 | 16 | description=("Package for easily generating and manipulating signal " 17 | "pulses. Developed for use with qubits in the quantum " 18 | "computing labs of Copenhagen, Delft, and Sydney, but " 19 | "should be generally useable."), 20 | 21 | license='MIT', 22 | 23 | packages=['broadbean'], 24 | 25 | classifiers=[ 26 | 'Development Status :: 4 - Beta', 27 | 'Intended Audience :: Science/Research', 28 | 'Programming Language :: Python :: 3.6' 29 | ], 30 | 31 | keywords='Pulsebuilding signal processing arbitrary waveforms', 32 | 33 | url='https://github.com/QCoDeS/broadbean' 34 | ) 35 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # Carefully tested on 18 May 2017 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = broadbean 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | GH_PAGES_SOURCES = docs broadbean 11 | 12 | # Put it first so that "make" without argument is like "make help". 13 | help: 14 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 15 | 16 | .PHONY: help Makefile 17 | 18 | 19 | .PHONY: html 20 | html: 21 | $(SPHINXBUILD) -b html $(SPHINXOPTS) . $(BUILDDIR)/html 22 | @echo 23 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 24 | 25 | 26 | gh-pages: 27 | git config --global user.email "bot@travics.com" 28 | git config --global user.name "Documentation Bot" 29 | git remote add upstream https://${GITHUB_API_KEY}@github.com/QCoDeS/broadbean.git 30 | git fetch upstream 31 | git checkout gh-pages 32 | rm -rf ./* 33 | git checkout master $(GH_PAGES_SOURCES) 34 | git reset HEAD 35 | cd docs && make html 36 | mv -fv docs/$(BUILDDIR)/html/* ../ 37 | rm -rf $(GH_PAGES_SOURCES) build 38 | git add -A 39 | git commit -m "Generated gh-pages for `git log master -1 --pretty=short \ 40 | --abbrev-commit`" && git push https://${GITHUB_API_KEY}@github.com/QCoDeS/broadbean.git gh-pages ; git checkout master 41 | 42 | 43 | .PHONY: execute 44 | execute: 45 | echo "Running the notebooks..." 46 | jupyter nbconvert --to notebook --execute Pulse\ Building\ Tutorial.ipynb 47 | jupyter nbconvert --to notebook --execute Filter\ compensation.ipynb 48 | jupyter nbconvert --to notebook --execute Subsequences.ipynb 49 | echo "Cleaning up the generated output..." 50 | rm *nbconvert.ipynb 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## QCoDeS Pulsebuilder aka broadbean 2 | 3 | [![Documentation Status](https://readthedocs.org/projects/broadbean/badge/?version=latest)](http://broadbean.readthedocs.io/en/latest/?badge=latest) 4 | 5 | A library for making pulses. Supposed to be used with QCoDeS (in 6 | particular its Tektronix AWG 5014 driver), but works as standalone. 7 | 8 | The usage is documented in the jupyter notebooks found in the `docs` folder. 9 | 10 | Short description: The broadbean module lets the user compose and 11 | manipulate pulse sequences. The aim of the module is to reduce pulse 12 | building to the logical minimum of specifications so that building and 13 | manipulation become as easy as saying "Gimme a square wave, then a 14 | ramp, then a sine, and then wait for 10 ms" and, in particular, "Do 15 | the same thing again, but now with the sine having twice the frequency 16 | it had before". 17 | 18 | The little extra module called `ripasso` performs frequency filtering 19 | and frequency filter compensation. It could be useful in a general 20 | setting and is therefore factored out to its own module. 21 | 22 | The name: The broad bean is one of my favourite pulses. 23 | 24 | ### Formal requirements 25 | 26 | The broadbean package only works with python 3.6+ 27 | 28 | ### Installation 29 | 30 | On a good day, installation is as easy as 31 | ``` 32 | $ git clone https://github.com/QCoDeS/broadbean.git bbdir 33 | $ cd bbdir 34 | $ pip install . 35 | ``` 36 | behind the scenes, `numpy`, `matplotlib`, and `PyQt5` are installed if 37 | not found. If `pip` failed you, you may need to run it as root. But a 38 | better idea is to use a [virtual enviroment](https://github.com/pyenv/pyenv-virtualenv). 39 | 40 | You can now fire up a python 3 interpreter and go 41 | ``` 42 | >>> import broadbean as bb 43 | >>> from broadbean import ripasso as rp 44 | ``` 45 | 46 | ### Documentation 47 | 48 | Apart from the example notebooks, auto-generated documentation is 49 | available. As for now, the user must built it herself, but that is 50 | luckily easy. 51 | 52 | In the `bbdir` folder, do: 53 | ``` 54 | $ pip install -r docs_requirements.txt 55 | $ cd docs 56 | $ make html 57 | ``` 58 | then ignore all warnings and just have a look at the file `bbdir/docs/build/html/index.html`. 59 | -------------------------------------------------------------------------------- /tests/test_awgfilegeneration.py: -------------------------------------------------------------------------------- 1 | # a small test that our output at least matches the signature it should match 2 | # before we make broadbean depend on QCoDeS, we can't directly test 3 | # that a valid .awg file is actually generated 4 | 5 | import pytest 6 | from hypothesis import given, settings 7 | import hypothesis.strategies as hst 8 | 9 | import broadbean as bb 10 | from broadbean.sequence import Sequence, SequencingError 11 | 12 | ramp = bb.PulseAtoms.ramp 13 | sine = bb.PulseAtoms.sine 14 | 15 | 16 | @pytest.fixture 17 | def protosequence1(): 18 | 19 | SR = 1e9 20 | 21 | th = bb.BluePrint() 22 | th.insertSegment(0, ramp, args=(0, 0), name='ramp', dur=10e-6) 23 | th.insertSegment(1, ramp, args=(1, 1), name='ramp', dur=5e-6) 24 | th.insertSegment(2, ramp, args=(0, 0), name='ramp', dur=10e-6) 25 | th.setSR(SR) 26 | 27 | wiggle1 = bb.BluePrint() 28 | wiggle1.insertSegment(0, sine, args=(4e6, 0.5, 0, 0), dur=25e-6) 29 | wiggle1.setSR(SR) 30 | 31 | wiggle2 = bb.BluePrint() 32 | wiggle2.insertSegment(0, sine, args=(8e6, 0.5, 0, 0), dur=25e-6) 33 | wiggle2.setSR(SR) 34 | 35 | elem1 = bb.Element() 36 | elem1.addBluePrint(1, th) 37 | elem1.addBluePrint(2, wiggle1) 38 | 39 | elem2 = bb.Element() 40 | elem2.addBluePrint(1, th) 41 | elem2.addBluePrint(2, wiggle2) 42 | 43 | seq = Sequence() 44 | seq.addElement(1, elem1) 45 | seq.addElement(2, elem2) 46 | seq.setSR(SR) 47 | seq.name = 'protoSequence' 48 | 49 | seq.setChannelAmplitude(1, 2) 50 | seq.setChannelAmplitude(2, 2) 51 | seq.setChannelOffset(1, 0) 52 | seq.setChannelOffset(2, 0) 53 | seq.setSequencingTriggerWait(1, 1) 54 | seq.setSequencingTriggerWait(2, 1) 55 | seq.setSequencingEventJumpTarget(1, 1) 56 | seq.setSequencingEventJumpTarget(2, 1) 57 | seq.setSequencingGoto(1, 1) 58 | seq.setSequencingGoto(2, 1) 59 | 60 | return seq 61 | 62 | 63 | def test_awg_output(protosequence1): 64 | 65 | # basic check: no exceptions should be raised 66 | package = protosequence1.outputForAWGFile() 67 | 68 | tst = package[1] 69 | 70 | assert isinstance(tst, tuple) 71 | assert len(tst) == 7 72 | 73 | 74 | def should_raise_sequencingerror(wait, nrep, jump_to, goto, num_elms): 75 | """ 76 | Function to tell us whether a SequencingError should be raised 77 | """ 78 | if wait not in [0, 1]: 79 | return True 80 | if nrep not in range(0, 16384): 81 | return True 82 | if jump_to not in range(-1, num_elms+1): 83 | return True 84 | if goto not in range(0, num_elms+1): 85 | return True 86 | return False 87 | 88 | 89 | @settings(max_examples=25) 90 | @given(wait=hst.integers(), nrep=hst.integers(), jump_to=hst.integers(), 91 | goto=hst.integers()) 92 | def test_awg_output_validations(protosequence1, wait, nrep, jump_to, goto): 93 | 94 | protosequence1.setSequencingTriggerWait(1, wait) 95 | protosequence1.setSequencingNumberOfRepetitions(1, nrep) 96 | protosequence1.setSequencingEventJumpTarget(1, jump_to) 97 | protosequence1.setSequencingGoto(1, goto) 98 | 99 | N = protosequence1.length_sequenceelements 100 | 101 | if should_raise_sequencingerror(wait, nrep, jump_to, goto, N): 102 | with pytest.raises(SequencingError): 103 | protosequence1.outputForAWGFile() 104 | else: 105 | protosequence1.outputForAWGFile() 106 | -------------------------------------------------------------------------------- /tests/test_subsequences.py: -------------------------------------------------------------------------------- 1 | # Test suite for everything subsequence related for broadbean 2 | # Sequences 3 | 4 | import pytest 5 | import numpy as np 6 | 7 | import broadbean as bb 8 | from broadbean.sequence import fs_schema, Sequence 9 | 10 | ramp = bb.PulseAtoms.ramp 11 | sine = bb.PulseAtoms.sine 12 | gauss = bb.PulseAtoms.gaussian 13 | 14 | SR1 = 1e9 15 | 16 | forged_sequence_schema = fs_schema 17 | 18 | 19 | @pytest.fixture 20 | def subseq1(): 21 | """ 22 | A small sequence meant to be used as a subsequence 23 | """ 24 | 25 | longdur = 201e-9 26 | 27 | wait = bb.BluePrint() 28 | wait.insertSegment(0, ramp, args=(0, 0), dur=10e-9) 29 | wait.setSR(SR1) 30 | 31 | wiggle = bb.BluePrint() 32 | wiggle.insertSegment(0, sine, args=(10e6, 10e-3, 0, 0), dur=longdur) 33 | wiggle.setSR(SR1) 34 | 35 | blob = bb.BluePrint() 36 | blob.insertSegment(0, gauss, args=(25e-3, 12e-9, 0, 0), dur=longdur) 37 | blob.setSR(SR1) 38 | 39 | slope = bb.BluePrint() 40 | slope.insertSegment(0, ramp, (0, 15e-3), dur=longdur) 41 | slope.setSR(SR1) 42 | 43 | elem1 = bb.Element() 44 | elem1.addBluePrint(1, wait) 45 | elem1.addBluePrint(2, wait) 46 | elem1.addBluePrint(3, wait) 47 | 48 | elem2 = bb.Element() 49 | elem2.addBluePrint(1, wiggle) 50 | elem2.addBluePrint(2, slope) 51 | elem2.addBluePrint(3, blob) 52 | 53 | elem3 = elem1.copy() 54 | 55 | seq = Sequence() 56 | seq.setSR(SR1) 57 | seq.addElement(1, elem1) 58 | seq.addElement(2, elem2) 59 | seq.addElement(3, elem3) 60 | seq.setSequencingNumberOfRepetitions(1, 10) 61 | seq.setSequencingNumberOfRepetitions(3, 10) 62 | 63 | return seq 64 | 65 | 66 | @pytest.fixture 67 | def subseq2(): 68 | """ 69 | A small sequence meant to be used as a subsequence 70 | """ 71 | 72 | longdur = 101e-9 73 | 74 | wait = bb.BluePrint() 75 | wait.insertSegment(0, ramp, args=(0, 0), dur=10e-9) 76 | wait.setSR(SR1) 77 | 78 | wiggle = bb.BluePrint() 79 | wiggle.insertSegment(0, sine, args=(10e6, 10e-3, 0, 0), dur=longdur) 80 | wiggle.setSR(SR1) 81 | 82 | blob = bb.BluePrint() 83 | blob.insertSegment(0, gauss, args=(25e-3, 12e-9, 0, 0), dur=longdur) 84 | blob.setSR(SR1) 85 | 86 | slope = bb.BluePrint() 87 | slope.insertSegment(0, ramp, (0, 15e-3), dur=longdur) 88 | slope.setSR(SR1) 89 | 90 | elem1 = bb.Element() 91 | elem1.addBluePrint(1, wait) 92 | elem1.addBluePrint(2, wait) 93 | elem1.addBluePrint(3, wait) 94 | 95 | elem2 = bb.Element() 96 | elem2.addBluePrint(1, wiggle) 97 | elem2.addBluePrint(2, slope) 98 | elem2.addBluePrint(3, blob) 99 | 100 | seq = Sequence() 101 | seq.setSR(SR1) 102 | seq.addElement(1, elem2) 103 | seq.addElement(2, elem1) 104 | seq.setSequencingNumberOfRepetitions(2, 15) 105 | 106 | return seq 107 | 108 | 109 | @pytest.fixture 110 | def noise_element(): 111 | """ 112 | An element consisting of arrays of noise 113 | """ 114 | 115 | noise1 = np.random.randn(250) 116 | noise2 = np.random.randn(250) 117 | noise3 = np.random.randn(250) 118 | 119 | elem = bb.Element() 120 | elem.addArray(1, noise1, SR=SR1) 121 | elem.addArray(2, noise2, SR=SR1) 122 | elem.addArray(3, noise3, SR=SR1) 123 | 124 | return elem 125 | 126 | 127 | @pytest.fixture 128 | def bp_element(): 129 | 130 | dur = 100e-9 131 | 132 | bp1 = bb.BluePrint() 133 | bp1.insertSegment(0, sine, (1e6, 10e-3, 0, 0), dur=dur) 134 | 135 | bp2 = bb.BluePrint() 136 | bp2.insertSegment(0, sine, (2e6, 10e-3, 0, np.pi/2), dur=dur) 137 | 138 | bp3 = bb.BluePrint() 139 | bp3.insertSegment(0, sine, (3e6, 10e-3, 0, -1), dur=dur) 140 | 141 | for bp in [bp1, bp2, bp3]: 142 | bp.setSR(SR1) 143 | 144 | elem = bb.Element() 145 | for ch, bp in enumerate([bp1, bp2, bp3]): 146 | elem.addBluePrint(ch+1, bp) 147 | 148 | return elem 149 | 150 | 151 | @pytest.fixture 152 | def master_sequence(subseq1, subseq2, bp_element, noise_element): 153 | """ 154 | A sequence with subsequences and elements, some elements 155 | have ararys, some have blueprint. We try to aim wide. 156 | """ 157 | 158 | seq = Sequence() 159 | seq.setSR(SR1) 160 | 161 | seq.addElement(1, noise_element) 162 | seq.addSubSequence(2, subseq1) 163 | seq.addElement(3, bp_element) 164 | seq.addSubSequence(4, subseq2) 165 | 166 | return seq 167 | 168 | 169 | def test_forge(master_sequence): 170 | 171 | assert master_sequence.length_sequenceelements == 4 172 | 173 | forged_seq = master_sequence.forge() 174 | 175 | forged_sequence_schema.validate(forged_seq) 176 | -------------------------------------------------------------------------------- /tests/test_forging.py: -------------------------------------------------------------------------------- 1 | # This test suite is meant to test everything related to forging, i.e. making 2 | # numpy arrays out of BluePrints 3 | 4 | from hypothesis import given 5 | import hypothesis.strategies as hst 6 | import pytest 7 | import numpy as np 8 | 9 | import broadbean as bb 10 | from broadbean.blueprint import _subelementBuilder, SegmentDurationError 11 | from broadbean.ripasso import applyInverseRCFilter 12 | from broadbean.sequence import Sequence 13 | 14 | import matplotlib.pyplot as plt 15 | plt.ion() 16 | 17 | ramp = bb.PulseAtoms.ramp 18 | sine = bb.PulseAtoms.sine 19 | 20 | 21 | @pytest.fixture 22 | def sequence_maker(): 23 | """ 24 | Return a function returning a sequence with some top hat pulses 25 | """ 26 | 27 | def make_seq(seqlen, channels, SR): 28 | 29 | seq = Sequence() 30 | seq.setSR(SR) 31 | 32 | for pos in range(1, seqlen+1): 33 | 34 | elem = bb.Element() 35 | 36 | for chan in channels: 37 | bp = bb.BluePrint() 38 | bp.insertSegment(-1, ramp, (0, 0), dur=20/SR) 39 | bp.insertSegment(-1, ramp, (1, 1), dur=10/SR) 40 | bp.insertSegment(-1, ramp, (0, 0), dur=5/SR) 41 | bp.setSR(SR) 42 | elem.addBluePrint(chan, bp) 43 | 44 | seq.addElement(pos, elem) 45 | 46 | return seq 47 | 48 | return make_seq 49 | 50 | 51 | def _has_period(array: np.ndarray, period: int) -> bool: 52 | """ 53 | Check whether an array has a specific period 54 | """ 55 | try: 56 | array = array.reshape((len(array)//period, period)) 57 | except ValueError: 58 | return False 59 | 60 | for n in range(period): 61 | column = array[:, n] 62 | if not np.allclose(column, column[0]): 63 | return False 64 | 65 | return True 66 | 67 | 68 | @given(SR=hst.integers(min_value=100, max_value=50e9), 69 | ratio=hst.floats(min_value=1e-6, max_value=10)) 70 | def test_too_short_durations_rejected(SR, ratio): 71 | 72 | # Any ratio larger than 1 will be rounded up by 73 | # _subelementBuilder to yield two points 74 | # (is that desired?) 75 | shortdur = ratio*1/SR 76 | 77 | bp = bb.BluePrint() 78 | bp.setSR(SR) 79 | bp.insertSegment(0, ramp, (0, 1), dur=shortdur) 80 | 81 | if ratio < 1.5: 82 | with pytest.raises(SegmentDurationError): 83 | _subelementBuilder(bp, SR, [shortdur]) 84 | else: 85 | _subelementBuilder(bp, SR, [shortdur]) 86 | 87 | 88 | def test_correct_periods(): 89 | 90 | SR = 1e9 91 | dur = 100e-9 92 | freqs = [100e6, 200e6, 500e6] 93 | periods = [int(SR/freq) for freq in freqs] 94 | 95 | for freq, period in zip(freqs, periods): 96 | bp = bb.BluePrint() 97 | bp.insertSegment(0, sine, (freq, 1, 0, 0), dur=dur) 98 | bp.setSR(SR) 99 | 100 | wfm = _subelementBuilder(bp, SR, [dur])['wfm'] 101 | 102 | assert _has_period(wfm, period) 103 | 104 | 105 | def test_correct_marker_times(): 106 | 107 | SR = 100 108 | 109 | bp = bb.BluePrint() 110 | bp.insertSegment(-1, ramp, (0, 0), dur=1, name='A') 111 | bp.insertSegment(-1, ramp, (0, 0), dur=1, name='B') 112 | bp.insertSegment(-1, ramp, (0, 0), dur=1, name='C') 113 | bp.setSR(SR) 114 | 115 | bp.setSegmentMarker('A', (0, 0.5), 1) 116 | bp.setSegmentMarker('B', (-0.1, 0.25), 2) 117 | bp.setSegmentMarker('C', (0.1, 0.25), 1) 118 | 119 | forged_bp = _subelementBuilder(bp, SR, [1, 1, 1]) 120 | 121 | m1 = forged_bp['m1'] 122 | 123 | assert (m1 == np.concatenate((np.ones(50), np.zeros(160), 124 | np.ones(25), np.zeros(65)))).all() 125 | 126 | 127 | def test_apply_filters_in_forging(sequence_maker): 128 | """ 129 | Assign some filters, forge and assert that they were applied 130 | """ 131 | N = 5 132 | channels = [1, 2, 'my_channel'] 133 | filter_orders = [1, 2, 3] 134 | SR = 1e9 135 | 136 | seq = sequence_maker(N, channels, SR) 137 | 138 | for chan, order in zip(channels, filter_orders): 139 | seq.setChannelFilterCompensation(chan, kind='HP', 140 | order=order, f_cut=SR/10, 141 | tau=None) 142 | 143 | forged_seq_bare = seq.forge(apply_filters=False) 144 | forged_seq_filtered = seq.forge(apply_filters=True) 145 | 146 | for chan, order in zip(channels, filter_orders): 147 | 148 | wfm_bare = forged_seq_bare[1]['content'][1]['data'][chan]['wfm'] 149 | expected = applyInverseRCFilter(wfm_bare, SR, kind='HP', 150 | f_cut=SR/10, order=order, DCgain=1) 151 | 152 | forged = forged_seq_filtered[1]['content'][1]['data'][chan]['wfm'] 153 | 154 | assert np.all(expected == forged) 155 | -------------------------------------------------------------------------------- /broadbean/broadbean.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import warnings 3 | from typing import List, Dict, Union, Callable 4 | from inspect import signature 5 | from copy import deepcopy 6 | import functools as ft 7 | 8 | import numpy as np 9 | 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | class PulseAtoms: 15 | """ 16 | A class full of static methods. 17 | The basic pulse shapes. 18 | 19 | Any pulse shape function should return a list or an np.array 20 | and have SR, npoints as its final two arguments. 21 | 22 | Rounding errors are a real concern/pain in the business of 23 | making waveforms of short duration (few samples). Therefore, 24 | the PulseAtoms take the number of points rather than the 25 | duration as input argument, so that all ambiguity can be handled 26 | in one place (the _subelementBuilder) 27 | """ 28 | 29 | @staticmethod 30 | def sine(freq, ampl, off, phase, SR, npts): 31 | time = np.linspace(0, npts/SR, npts, endpoint=False) 32 | freq *= 2*np.pi 33 | return (ampl*np.sin(freq*time+phase)+off) 34 | 35 | @staticmethod 36 | def ramp(start, stop, SR, npts): 37 | dur = npts/SR 38 | slope = (stop-start)/dur 39 | time = np.linspace(0, dur, npts, endpoint=False) 40 | return (slope*time+start) 41 | 42 | @staticmethod 43 | def waituntil(dummy, SR, npts): 44 | # for internal call signature consistency, a dummy variable is needed 45 | return np.zeros(int(npts)) 46 | 47 | @staticmethod 48 | def gaussian(ampl, sigma, mu, offset, SR, npts): 49 | """ 50 | Returns a Gaussian of peak height ampl (when offset==0) 51 | 52 | Is by default centred in the middle of the interval 53 | """ 54 | dur = npts/SR 55 | time = np.linspace(0, dur, npts, endpoint=False) 56 | centre = dur/2 57 | baregauss = np.exp((-(time-mu-centre)**2/(2*sigma**2))) 58 | return ampl*baregauss+offset 59 | 60 | 61 | def marked_for_deletion(replaced_by: Union[str, None]=None) -> Callable: 62 | """ 63 | A decorator for functions we want to kill. The function still 64 | gets called. 65 | """ 66 | def decorator(func): 67 | @ft.wraps(func) 68 | def warner(*args, **kwargs): 69 | warnstr = f'{func.__name__} is obsolete.' 70 | if replaced_by: 71 | warnstr += f' Please use {replaced_by} insted.' 72 | warnings.warn(warnstr) 73 | return func(*args, **kwargs) 74 | return warner 75 | return decorator 76 | 77 | 78 | def _channelListSorter(channels: List[Union[str, int]]) -> List[Union[str, int]]: 79 | """ 80 | Sort a list of channel names. Channel names can be ints or strings. Sorts 81 | ints as being before strings. 82 | """ 83 | intlist: List[Union[str, int]] = [] 84 | intlist = [ch for ch in channels if isinstance(ch, int)] 85 | strlist: List[Union[str, int]] = [] 86 | strlist = [ch for ch in channels if isinstance(ch, str)] 87 | 88 | sorted_list = sorted(intlist) + sorted(strlist) 89 | 90 | return sorted_list 91 | 92 | 93 | class _AWGOutput: 94 | """ 95 | Class used inside Sequence.outputForAWGFile 96 | 97 | Allows for easy-access slicing to return several valid tuples 98 | for the QCoDeS Tektronix AWG 5014 driver from the same sequence. 99 | 100 | Example: 101 | A sequence, myseq, specifies channels 1, 2, 3, 4. 102 | 103 | out = myseq.outputForAWGFile() 104 | 105 | out[:] <--- tuple with all channels 106 | out[1:3] <--- tuple with channels 1, 2 107 | out[2] <--- tuple with channel 2 108 | """ 109 | 110 | def __init__(self, rawpackage, channels): 111 | """ 112 | Rawpackage is a tuple: 113 | (wfms, m1s, m2s, nreps, trig_wait, goto, jump) 114 | 115 | Channels is a list of what the channels were called in their 116 | sequence object whence this instance is created 117 | """ 118 | 119 | self.channels = channels 120 | 121 | self._channels = {} 122 | for ii in range(len(rawpackage[0])): 123 | self._channels[ii] = {'wfms': rawpackage[0][ii], 124 | 'm1s': rawpackage[1][ii], 125 | 'm2s': rawpackage[2][ii]} 126 | self.nreps = rawpackage[3] 127 | self.trig_wait = rawpackage[4] 128 | self.goto = rawpackage[5] 129 | self.jump = rawpackage[6] 130 | 131 | def __getitem__(self, key): 132 | 133 | if isinstance(key, int): 134 | if key in self._channels.keys(): 135 | output = ([self._channels[key]['wfms']], 136 | [self._channels[key]['m1s']], 137 | [self._channels[key]['m2s']], 138 | self.nreps, self.trig_wait, self.goto, self.jump) 139 | 140 | return output 141 | else: 142 | raise KeyError('{} is not a valid key.'.format(key)) 143 | 144 | if isinstance(key, slice): 145 | start = key.start 146 | if start is None: 147 | start = 0 148 | 149 | stop = key.stop 150 | if stop is None: 151 | stop = len(self._channels.keys()) 152 | 153 | step = key.step 154 | if step is None: 155 | step = 1 156 | 157 | indeces = range(start, stop, step) 158 | 159 | wfms = [self._channels[ind]['wfms'] for ind in indeces] 160 | m1s = [self._channels[ind]['m1s'] for ind in indeces] 161 | m2s = [self._channels[ind]['m2s'] for ind in indeces] 162 | 163 | output = (wfms, m1s, m2s, 164 | self.nreps, self.trig_wait, self.goto, self.jump) 165 | 166 | return output 167 | 168 | raise KeyError('Key must be int or slice!') 169 | 170 | 171 | @marked_for_deletion(replaced_by='broadbean.plotting.plotter') 172 | def bluePrintPlotter(blueprints, fig=None, axs=None): 173 | pass 174 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # broadbean documentation build configuration file, created by 5 | # sphinx-quickstart on Wed May 17 09:11:27 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = ['sphinx.ext.autodoc', 35 | 'sphinx.ext.ifconfig', 36 | 'sphinx.ext.viewcode'] 37 | 38 | # Add any paths that contain templates here, relative to this directory. 39 | templates_path = ['_sphinx_templates'] 40 | 41 | # The suffix(es) of source filenames. 42 | # You can specify multiple suffix as a list of string: 43 | # 44 | # source_suffix = ['.rst', '.md'] 45 | source_suffix = '.rst' 46 | 47 | # The master toctree document. 48 | master_doc = 'index' 49 | 50 | # General information about the project. 51 | project = 'broadbean' 52 | copyright = '2017, William H.P. Nielsen' 53 | author = 'William H.P. Nielsen' 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | # The short X.Y version. 60 | version = '0.9' 61 | # The full version, including alpha/beta/rc tags. 62 | release = '0.9.1' 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = None 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | # This patterns also effect to html_static_path and html_extra_path 74 | exclude_patterns = [] 75 | 76 | # The name of the Pygments (syntax highlighting) style to use. 77 | pygments_style = 'sphinx' 78 | 79 | # If true, `todo` and `todoList` produce output, else they produce nothing. 80 | todo_include_todos = True 81 | 82 | # Extensions, here we enable napoleon to have Google style docstrings 83 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx.ext.todo'] 84 | 85 | # -- Options for HTML output ---------------------------------------------- 86 | 87 | # The theme to use for HTML and HTML Help pages. See the documentation for 88 | # a list of builtin themes. 89 | # 90 | # html_theme = 'alabaster' 91 | 92 | # Theme options are theme-specific and customize the look and feel of a theme 93 | # further. For a list of options available for each theme, see the 94 | # documentation. 95 | # 96 | # html_theme_options = {} 97 | html_theme = "sphinx_rtd_theme" 98 | html_theme_path = ["_sphinx_themes"] 99 | 100 | # Add any paths that contain custom static files (such as style sheets) here, 101 | # relative to this directory. They are copied after the builtin static files, 102 | # so a file named "default.css" will overwrite the builtin "default.css". 103 | html_static_path = ['_sphinx_static'] 104 | 105 | 106 | # -- Options for HTMLHelp output ------------------------------------------ 107 | 108 | # Output file base name for HTML help builder. 109 | htmlhelp_basename = 'broadbeandoc' 110 | 111 | 112 | # -- Options for LaTeX output --------------------------------------------- 113 | 114 | latex_elements = { 115 | # The paper size ('letterpaper' or 'a4paper'). 116 | # 117 | # 'papersize': 'letterpaper', 118 | 119 | # The font size ('10pt', '11pt' or '12pt'). 120 | # 121 | # 'pointsize': '10pt', 122 | 123 | # Additional stuff for the LaTeX preamble. 124 | # 125 | # 'preamble': '', 126 | 127 | # Latex figure (float) alignment 128 | # 129 | # 'figure_align': 'htbp', 130 | } 131 | 132 | # Grouping the document tree into LaTeX files. List of tuples 133 | # (source start file, target name, title, 134 | # author, documentclass [howto, manual, or own class]). 135 | latex_documents = [ 136 | (master_doc, 'broadbean.tex', 'broadbean Documentation', 137 | 'William H.P. Nielsen', 'manual'), 138 | ] 139 | 140 | 141 | # -- Options for manual page output --------------------------------------- 142 | 143 | # One entry per manual page. List of tuples 144 | # (source start file, name, description, authors, manual section). 145 | man_pages = [ 146 | (master_doc, 'broadbean', 'broadbean Documentation', 147 | [author], 1) 148 | ] 149 | 150 | 151 | # -- Options for Texinfo output ------------------------------------------- 152 | 153 | # Grouping the document tree into Texinfo files. List of tuples 154 | # (source start file, target name, title, author, 155 | # dir menu entry, description, category) 156 | texinfo_documents = [ 157 | (master_doc, 'broadbean', 'broadbean Documentation', 158 | author, 'broadbean', 'One line description of project.', 159 | 'Miscellaneous'), 160 | ] 161 | 162 | 163 | # -- Options for Epub output ---------------------------------------------- 164 | 165 | # Bibliographic Dublin Core info. 166 | epub_title = project 167 | epub_author = author 168 | epub_publisher = author 169 | epub_copyright = copyright 170 | 171 | # The unique identifier of the text. This can be a ISBN number 172 | # or the project homepage. 173 | # 174 | # epub_identifier = '' 175 | 176 | # A unique identification for the text. 177 | # 178 | # epub_uid = '' 179 | 180 | # A list of files that should not be packed into the epub file. 181 | epub_exclude_files = ['search.html'] 182 | -------------------------------------------------------------------------------- /tests/test_element.py: -------------------------------------------------------------------------------- 1 | # Test suite for the Element Object of the broadband package 2 | # 3 | # We let the test_blueprint.py test the BluePrint and only test that Elements 4 | # work on the Element level 5 | 6 | import pytest 7 | import broadbean as bb 8 | import numpy as np 9 | from broadbean.element import ElementDurationError, Element 10 | from hypothesis import given, settings 11 | import hypothesis.strategies as hst 12 | 13 | ramp = bb.PulseAtoms.ramp 14 | sine = bb.PulseAtoms.sine 15 | tophat_SR = 2000 16 | 17 | 18 | @pytest.fixture(scope='function') 19 | def blueprint_tophat(): 20 | """ 21 | Return a blueprint consisting of three slopeless ramps forming something 22 | similar to a tophat 23 | """ 24 | th = bb.BluePrint() 25 | th.insertSegment(0, ramp, args=(0, 0), name='ramp', dur=1) 26 | th.insertSegment(1, ramp, args=(1, 1), name='ramp', dur=0.5) 27 | th.insertSegment(2, ramp, args=(0, 0), name='ramp', dur=1) 28 | th.setSR(tophat_SR) 29 | 30 | return th 31 | 32 | 33 | @pytest.fixture 34 | def mixed_element(blueprint_tophat): 35 | """ 36 | An element with blueprints and arrays 37 | """ 38 | 39 | noise = np.random.randn(blueprint_tophat.points) 40 | wiggle = bb.BluePrint() 41 | wiggle.insertSegment(0, sine, args=(1, 10, 0, 0), dur=2.5) 42 | wiggle.setSR(blueprint_tophat.SR) 43 | 44 | elem = Element() 45 | elem.addBluePrint(1, blueprint_tophat) 46 | elem.addArray(2, noise, blueprint_tophat.SR) 47 | elem.addBluePrint(3, wiggle) 48 | 49 | return elem 50 | 51 | 52 | ################################################## 53 | # TEST BARE INITIALISATION 54 | 55 | 56 | def test_bare_init(blueprint_tophat): 57 | elem = Element() 58 | elem.addBluePrint(1, blueprint_tophat) 59 | assert list(elem._data.keys()) == [1] 60 | 61 | 62 | def test_equality_true(blueprint_tophat): 63 | elem1 = Element() 64 | elem2 = Element() 65 | elem1.addBluePrint(1, blueprint_tophat) 66 | elem2.addBluePrint(1, blueprint_tophat) 67 | assert elem1 == elem2 68 | 69 | 70 | def test_equality_false(blueprint_tophat): 71 | elem1 = Element() 72 | elem2 = Element() 73 | elem1.addBluePrint(1, blueprint_tophat) 74 | elem2.addBluePrint(1, blueprint_tophat) 75 | elem1.changeArg(1, 'ramp', 'start', 2) 76 | assert elem1 != elem2 77 | 78 | 79 | def test_copy(blueprint_tophat): 80 | elem1 = Element() 81 | elem1.addBluePrint(1, blueprint_tophat) 82 | elem2 = elem1.copy() 83 | assert elem1 == elem2 84 | 85 | ################################################## 86 | # Adding things to the Element goes hand in hand 87 | # with duration validation 88 | 89 | 90 | def test_addArray(): 91 | 92 | SR = 1e9 93 | N = 2500 94 | 95 | wfm = np.linspace(0, N/SR, N) 96 | m1 = np.zeros(N) 97 | m2 = np.ones(N) 98 | 99 | elem = Element() 100 | elem.addArray(1, wfm, SR, m1=m1, m2=m2) 101 | elem.addArray('2', wfm, SR, m1=m1) 102 | elem.addArray('readout_channel', wfm, SR, m2=m2) 103 | 104 | elem.validateDurations() 105 | 106 | M = 2400 107 | wfm2 = np.linspace(0, M/SR, M) 108 | elem.addArray(3, wfm2, SR) 109 | 110 | with pytest.raises(ElementDurationError): 111 | elem.validateDurations() 112 | 113 | with pytest.raises(ValueError): 114 | elem.addArray(1, wfm, SR, m1=m1[:-1]) 115 | 116 | with pytest.raises(ValueError): 117 | elem.addArray(2, wfm, SR, m2=m2[3:]) 118 | 119 | 120 | @settings(max_examples=25) 121 | @given(SR1=hst.integers(1), SR2=hst.integers(1), 122 | N=hst.integers(2), M=hst.integers(2)) 123 | def test_invalid_durations(SR1, SR2, N, M): 124 | """ 125 | There are soooo many ways to have invalid durations, here 126 | we hit a couple of them 127 | """ 128 | 129 | # differing sample rates 130 | 131 | elem = Element() 132 | bp = bb.BluePrint() 133 | 134 | bp.insertSegment(0, ramp, (0, 0), dur=N/SR2) 135 | bp.setSR(SR2) 136 | 137 | wfm = np.linspace(-1, 1, N) 138 | elem.addArray(1, wfm, SR1) 139 | elem.addBluePrint(2, bp) 140 | 141 | if SR1 == SR2: 142 | elem.validateDurations() 143 | else: 144 | with pytest.raises(ElementDurationError): 145 | elem.validateDurations() 146 | 147 | # differing durations 148 | bp1 = bb.BluePrint() 149 | bp1.insertSegment(0, ramp, (0, 1), dur=N/SR1) 150 | bp1.setSR(SR1) 151 | 152 | bp2 = bb.BluePrint() 153 | bp2.insertSegment(0, ramp, (0, 2), dur=M/SR1) 154 | bp2.setSR(SR1) 155 | 156 | elem = Element() 157 | elem.addBluePrint(1, bp1) 158 | elem.addBluePrint(2, bp2) 159 | 160 | if N == M: 161 | elem.validateDurations() 162 | else: 163 | with pytest.raises(ElementDurationError): 164 | elem.validateDurations() 165 | 166 | 167 | def test_applyDelays(mixed_element): 168 | 169 | delays = [1e-1, 0, 0] 170 | 171 | assert mixed_element.duration == 2.5 172 | 173 | arrays_before = mixed_element.getArrays() 174 | assert len(arrays_before[1]['wfm']) == 5000 175 | 176 | with pytest.raises(ValueError): 177 | mixed_element._applyDelays([-0.1, 3, 4]) 178 | 179 | with pytest.raises(ValueError): 180 | mixed_element._applyDelays([0, 1]) 181 | 182 | element = mixed_element.copy() 183 | element._applyDelays(delays) 184 | 185 | arrays_after = element.getArrays() 186 | assert len(arrays_after[1]['wfm']) == 5200 187 | 188 | assert mixed_element.duration == 2.5 189 | assert element.duration == 2.6 190 | 191 | assert element._data[1]['blueprint'].length_segments == 4 192 | assert element._data[3]['blueprint'].length_segments == 2 193 | 194 | 195 | ################################################## 196 | # Input validation 197 | 198 | 199 | @pytest.mark.parametrize('improper_bp', [{1: 2}, 'blueprint', bb.BluePrint()]) 200 | def test_input_fail1(improper_bp): 201 | elem = Element() 202 | with pytest.raises(ValueError): 203 | elem.addBluePrint(1, improper_bp) 204 | 205 | ################################################## 206 | # Properties 207 | 208 | 209 | @settings(max_examples=25) 210 | @given(SR=hst.integers(1), N=hst.integers(2)) 211 | def test_points(SR, N): 212 | elem = Element() 213 | 214 | with pytest.raises(KeyError): 215 | elem.points 216 | 217 | bp = bb.BluePrint() 218 | 219 | bp.insertSegment(0, ramp, (0, 0), dur=N/SR) 220 | bp.setSR(SR) 221 | 222 | wfm = np.linspace(-1, 1, N) 223 | elem.addArray(1, wfm, SR) 224 | elem.addBluePrint(2, bp) 225 | 226 | assert elem.points == N 227 | 228 | elem = Element() 229 | bp = bb.BluePrint() 230 | 231 | bp.insertSegment(0, ramp, (0, 0), dur=N/SR) 232 | bp.setSR(SR) 233 | 234 | wfm = np.linspace(-1, 1, N) 235 | elem.addArray(2, wfm, SR) 236 | elem.addBluePrint(1, bp) 237 | 238 | assert elem.points == N 239 | -------------------------------------------------------------------------------- /broadbean/ripasso.py: -------------------------------------------------------------------------------- 1 | # Module providing filter compensation. Developed for use with the broadbean 2 | # pulse building module, but provides a standalone API 3 | # 4 | # The name is (of course) a pun. Ripasso; first a filter, then a compensation, 5 | # i.e. something that is re-passed. Also not quite an Amarone... 6 | # 7 | 8 | import numpy as np 9 | import matplotlib.pyplot as plt 10 | from numpy.fft import fft, ifft, fftfreq 11 | import logging 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | class MissingFrequenciesError(Exception): 17 | pass 18 | 19 | 20 | def _rcFilter(SR, npts, f_cut, kind='HP', order=1, DCgain=0): 21 | """ 22 | Nth order (RC circuit) filter 23 | made with frequencies matching the fft output 24 | """ 25 | 26 | freqs = fftfreq(npts, 1/SR) 27 | 28 | tau = 1/f_cut 29 | top = 2j*np.pi 30 | 31 | if kind == 'HP': 32 | tf = top*tau*freqs/(1+top*tau*freqs) 33 | 34 | # now, we have identically zero gain for the DC component, 35 | # which makes the transfer function non-invertible 36 | # 37 | # It is a bit of an open question what DC compensation we want... 38 | 39 | tf[tf == 0] = DCgain # No DC suppression 40 | 41 | elif kind == 'LP': 42 | tf = 1/(1+top*tau*freqs) 43 | 44 | return tf**order 45 | 46 | 47 | def applyRCFilter(signal, SR, kind, f_cut, order, DCgain=0): 48 | """ 49 | Apply a simple RC-circuit filter 50 | to signal and return the filtered signal. 51 | 52 | Args: 53 | signal (np.array): The input signal. The signal is assumed to start at 54 | t=0 and be evenly sampled at sample rate SR. 55 | SR (int): Sample rate (Sa/s) of the input signal 56 | kind (str): The type of filter. Either 'HP' or 'LP'. 57 | f_cut (float): The cutoff frequency of the filter (Hz) 58 | order (int): The order of the filter. The first order filter is 59 | applied order times. 60 | DCgain (Optional[float]): The DC gain of the filter. ONLY used by the 61 | high-pass filter. Default: 0. 62 | 63 | Returns: 64 | np.array: 65 | The filtered signal along the original time axis. Imaginary 66 | parts are discarded prior to return. 67 | 68 | Raises: 69 | ValueError: If kind is neither 'HP' nor 'LP' 70 | """ 71 | 72 | if kind not in ['HP', 'LP']: 73 | raise ValueError('Please specify filter type as either "HP" or "LP".') 74 | 75 | N = len(signal) 76 | transfun = _rcFilter(SR, N, f_cut, kind=kind, order=order, DCgain=DCgain) 77 | output = ifft(fft(signal)*transfun) 78 | output = np.real(output) 79 | 80 | return output 81 | 82 | 83 | def applyInverseRCFilter(signal, SR, kind, f_cut, order, DCgain=1): 84 | """ 85 | Apply the inverse of an RC-circuit filter to a signal and return the 86 | compensated signal. 87 | 88 | Note that a high-pass filter in principle has identically zero DC 89 | gain which requires an infinite offset to compensate. 90 | 91 | Args: 92 | signal (np.array): The input signal. The signal is assumed to start at 93 | t=0 and be evenly sampled at sample rate SR. 94 | SR (int): Sample rate (Sa/s) of the input signal 95 | kind (str): The type of filter. Either 'HP' or 'LP'. 96 | f_cut (float): The cutoff frequency of the filter (Hz) 97 | order (int): The order of the filter. The first order filter is 98 | applied order times. 99 | DCgain (Optional[float]): The DC gain of the filter. ONLY used by the 100 | high-pass filter. Default: 1. 101 | 102 | Returns: 103 | np.array: 104 | The filtered signal along the original time axis. Imaginary 105 | parts are discarded prior to return. 106 | 107 | Raises: 108 | ValueError: If kind is neither 'HP' nor 'LP' 109 | ValueError: If DCgain is zero. 110 | """ 111 | 112 | if kind not in ['HP', 'LP']: 113 | raise ValueError('Wrong filter type. ' 114 | 'Please specify filter type as either "HP" or "LP".') 115 | 116 | if not DCgain > 0: 117 | raise ValueError('Non-invertible DCgain! ' 118 | 'Please set DCgain to a finite value.') 119 | 120 | N = len(signal) 121 | transfun = _rcFilter(SR, N, f_cut, order=-order, kind=kind, DCgain=DCgain) 122 | output = ifft(fft(signal)*transfun) 123 | output = np.real(output) 124 | 125 | return output 126 | 127 | 128 | def applyCustomTransferFunction(signal, SR, tf_freqs, tf_amp, invert=False): 129 | """ 130 | Apply custom transfer function 131 | 132 | Given a signal, its sample rate, and a provided transfer function, apply 133 | the transfer function to the signal. 134 | 135 | Args: 136 | signal (np.array): A numpy array containing the signal 137 | SR (int): The sample rate of the signal (Sa/s) 138 | tf_freqs (np.array): The frequencies of the transfer function. Must 139 | be monotonically increasing. 140 | tf_amp (np.array): The amplitude of the transfer function. Must be 141 | dimensionless. 142 | invert (Optional[bool]): If True, the inverse transfer function is 143 | applied. Default: False. 144 | 145 | Returns: 146 | np.array: 147 | The modified signal. 148 | """ 149 | 150 | npts = len(signal) 151 | 152 | # validate tf_freqs 153 | 154 | df = np.diff(tf_freqs).round(6) 155 | 156 | if not np.sum(df > 0) == len(df): 157 | raise ValueError('Invalid transfer function freq. axis. ' 158 | 'Frequencies must be monotonically increasing.') 159 | 160 | if not tf_freqs[-1] >= SR/2: 161 | # TODO: think about whether this is a problem 162 | # What is the desired behaviour for high frequencies if nothing 163 | # is specified? I guess NOOP, i.e. the transfer func. is 1 164 | raise MissingFrequenciesError('Supplied transfer function does not ' 165 | 'specify frequency response up to the ' 166 | 'Nyquist frequency of the signal.') 167 | 168 | if not tf_freqs[0] == 0: 169 | # what to do in this case? Extrapolate 1s? Make the user do this? 170 | pass 171 | 172 | # Step 1: resample to fftfreq type axis 173 | freqax = fftfreq(npts, 1/SR) 174 | freqax_pos = freqax[:npts//2] 175 | freqax_neg = freqax[npts//2:] 176 | 177 | resampled_pos = np.interp(freqax_pos, tf_freqs, tf_amp) 178 | resampled_neg = np.interp(-freqax_neg[::-1], tf_freqs, tf_amp) 179 | 180 | transferfun = np.concatenate((resampled_pos, resampled_neg[::-1])) 181 | 182 | # Step 2: Apply transfer function 183 | if invert: 184 | power = -1 185 | else: 186 | power = 1 187 | 188 | signal_filtered = ifft(fft(signal)*(transferfun**power)) 189 | imax = np.imag(signal_filtered).max() 190 | log.debug('Applying custom transfer function. Discarding imag parts ' 191 | 'no larger than {}'.format(imax)) 192 | signal_filtered = np.real(signal_filtered) 193 | 194 | return signal_filtered 195 | -------------------------------------------------------------------------------- /broadbean/tools.py: -------------------------------------------------------------------------------- 1 | # High-level tool for sequence building and manipulation 2 | # 3 | 4 | import numpy as np 5 | import logging 6 | 7 | from broadbean.sequence import (Sequence, SequenceConsistencyError) 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | 12 | def makeLinearlyVaryingSequence(baseelement, channel, name, arg, start, stop, 13 | step): 14 | """ 15 | Make a pulse sequence where a single parameter varies linearly. 16 | The pulse sequence will consist of N copies of the same element with just 17 | the specified argument changed (N = abs(stop-start)/steps) 18 | 19 | Args: 20 | baseelement (Element): The basic element. 21 | channel (int): The channel where the change should happen 22 | name (str): Name of the blueprint segment to change 23 | arg (Union[str, int]): Name (str) or position (int) of the argument 24 | to change. If the arg is 'duration', the duration is changed 25 | instead. 26 | start (float): Start point of the variation (included) 27 | stop (float): Stop point of the variation (included) 28 | step (float): Increment of the variation 29 | """ 30 | 31 | # TODO: validation 32 | # TODO: Make more general varyer and refactor code 33 | 34 | sequence = Sequence() 35 | 36 | sequence.setSR(baseelement.SR) 37 | 38 | iterator = np.linspace(start, stop, round(abs(stop-start)/step)+1) 39 | 40 | for ind, val in enumerate(iterator): 41 | element = baseelement.copy() 42 | if arg == 'duration': 43 | element.changeDuration(channel, name, val) 44 | else: 45 | element.changeArg(channel, name, arg, val) 46 | sequence.addElement(ind+1, element) 47 | 48 | return sequence 49 | 50 | 51 | def makeVaryingSequence(baseelement, channels, names, args, iters): 52 | """ 53 | Make a pulse sequence where N parameters vary simultaneously in M steps. 54 | The user inputs a baseelement which is copied M times and changed 55 | according to the given inputs. 56 | 57 | Args: 58 | baseelement (Element): The basic element. 59 | channels (Union[list, tuple]): Either a list or a tuple of channels on 60 | which to find the blueprint to change. Must have length N. 61 | names (Union[list, tuple]): Either a list or a tuple of names of the 62 | segment to change. Must have length N. 63 | args (Union[list, tuple]): Either a list or a tuple of argument 64 | specifications for the argument to change. Use 'duration' to change 65 | the segment duration. Must have length N. 66 | iters (Union[list, tuple]): Either a list or a tuple of length N 67 | containing Union[list, tuple, range] of length M. 68 | 69 | Raises: 70 | ValueError: If not channels, names, args, and iters are of the same 71 | length. 72 | ValueError: If not each iter in iters specifies the same number of 73 | values. 74 | """ 75 | 76 | # Validation 77 | baseelement.validateDurations() 78 | 79 | inputlengths = [len(channels), len(names), len(args), len(iters)] 80 | if not inputlengths.count(inputlengths[0]) == len(inputlengths): 81 | raise ValueError('Inconsistent number of channel, names, args, and ' 82 | 'parameter sequences. Please specify the same number ' 83 | 'of each.') 84 | noofvals = [len(itr) for itr in iters] 85 | if not noofvals.count(noofvals[0]) == len(iters): 86 | raise ValueError('Not the same number of values in each parameter ' 87 | 'value sequence (input argument: iters)') 88 | 89 | sequence = Sequence() 90 | sequence.setSR(baseelement.SR) 91 | 92 | for elnum in range(1, noofvals[0]+1): 93 | sequence.addElement(elnum, baseelement.copy()) 94 | 95 | for (chan, name, arg, vals) in zip(channels, names, args, iters): 96 | for mpos, val in enumerate(vals): 97 | element = sequence.element(mpos+1) 98 | if arg == 'duration': 99 | element.changeDuration(chan, name, val) 100 | else: 101 | element.changeArg(chan, name, arg, val) 102 | 103 | log.info('Created varying sequence using makeVaryingSequence.' 104 | ' Now validating it...') 105 | 106 | if not sequence.checkConsistency(): 107 | raise SequenceConsistencyError('Invalid sequence. See log for ' 108 | 'details.') 109 | else: 110 | log.info('Valid sequence') 111 | return sequence 112 | 113 | 114 | def repeatAndVarySequence(seq, poss, channels, names, args, iters): 115 | """ 116 | Repeat a sequence and vary part(s) of it. Returns a new sequence. 117 | Given N specifications of M steps, N parameters are varied in M 118 | steps. 119 | 120 | Args: 121 | seq (Sequence): The sequence to be repeated. 122 | poss (Union[list, tuple]): A length N list/tuple specifying at which 123 | sequence position(s) the blueprint to change is. 124 | channels (Union[list, tuple]): A length N list/tuple specifying on 125 | which channel(s) the blueprint to change is. 126 | names (Union[list, tuple]): A length N list/tuple specifying the name 127 | of the segment to change. 128 | args (Union[list, tuple]): A length N list/tuple specifying which 129 | argument to change. A valid argument is also 'duration'. 130 | iters (Union[list, tuple]): A length N list/tuple containing length 131 | M indexable iterables with the values to step through. 132 | """ 133 | 134 | if not seq.checkConsistency(): 135 | raise SequenceConsistencyError('Inconsistent input sequence! Can not ' 136 | 'proceed. Check all positions ' 137 | 'and channels.') 138 | 139 | inputlens = [len(poss), len(channels), len(names), len(args), len(iters)] 140 | if not inputlens.count(inputlens[0]) == len(inputlens): 141 | raise ValueError('Inconsistent number of position, channel, name, args' 142 | ', and ' 143 | 'parameter sequences. Please specify the same number ' 144 | 'of each.') 145 | noofvals = [len(itr) for itr in iters] 146 | if not noofvals.count(noofvals[0]) == len(iters): 147 | raise ValueError('Not the same number of values in each parameter ' 148 | 'value sequence (input argument: iters)') 149 | 150 | newseq = Sequence() 151 | newseq._awgspecs = seq._awgspecs 152 | 153 | no_of_steps = noofvals[0] 154 | 155 | for step in range(no_of_steps): 156 | tempseq = seq.copy() 157 | for (pos, chan, name, arg, vals) in zip(poss, channels, names, 158 | args, iters): 159 | element = tempseq.element(pos) 160 | val = vals[step] 161 | 162 | if arg == 'duration': 163 | element.changeDuration(chan, name, val) 164 | else: 165 | element.changeArg(chan, name, arg, val) 166 | newseq = newseq + tempseq 167 | 168 | return newseq 169 | -------------------------------------------------------------------------------- /broadbean/plotting.py: -------------------------------------------------------------------------------- 1 | # A little helper module for plotting of broadbean objects 2 | 3 | from typing import Tuple, Union, Dict, List 4 | 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | 8 | from broadbean import Sequence, BluePrint, Element 9 | from broadbean.sequence import SequenceConsistencyError 10 | 11 | # The object we can/want to plot 12 | BBObject = Union[Sequence, BluePrint, Element] 13 | 14 | 15 | def getSIScalingAndPrefix(minmax: Tuple[float, float]) -> Tuple[float, str]: 16 | """ 17 | Return the scaling exponent and unit prefix. E.g. (-2e-3, 1e-6) will 18 | return (1e3, 'm') 19 | 20 | Args: 21 | minmax: The (min, max) value of the signal 22 | 23 | Returns: 24 | A tuple of the scaling (inverse of the prefix) and the prefix 25 | string. 26 | 27 | """ 28 | v_max = max(map(abs, minmax)) # type: ignore 29 | if v_max == 0: 30 | v_max = 1 # type: ignore 31 | exponent = np.log10(v_max) 32 | prefix = '' 33 | scaling: float = 1 34 | 35 | if exponent < 0: 36 | prefix = 'm' 37 | scaling = 1e3 38 | if exponent < -3: 39 | prefix = 'micro ' 40 | scaling = 1e6 41 | if exponent < -6: 42 | prefix = 'n' 43 | scaling = 1e9 44 | 45 | return (scaling, prefix) 46 | 47 | 48 | def _plot_object_validator(obj_to_plot: BBObject) -> None: 49 | """ 50 | Validate the object 51 | """ 52 | if isinstance(obj_to_plot, Sequence): 53 | proceed = obj_to_plot.checkConsistency(verbose=True) 54 | if not proceed: 55 | raise SequenceConsistencyError 56 | 57 | elif isinstance(obj_to_plot, Element): 58 | obj_to_plot.validateDurations() 59 | 60 | elif isinstance(obj_to_plot, BluePrint): 61 | assert obj_to_plot.SR is not None 62 | 63 | 64 | def _plot_object_forger(obj_to_plot: BBObject, 65 | **forger_kwargs) -> Dict[int, Dict]: 66 | """ 67 | Make a forged sequence out of any object. 68 | Returns a forged sequence. 69 | """ 70 | 71 | if isinstance(obj_to_plot, BluePrint): 72 | elem = Element() 73 | elem.addBluePrint(1, obj_to_plot) 74 | seq = Sequence() 75 | seq.addElement(1, elem) 76 | seq.setSR(obj_to_plot.SR) 77 | 78 | elif isinstance(obj_to_plot, Element): 79 | seq = Sequence() 80 | seq.addElement(1, obj_to_plot) 81 | seq.setSR(obj_to_plot._meta['SR']) 82 | 83 | elif isinstance(obj_to_plot, Sequence): 84 | seq = obj_to_plot 85 | 86 | forged_seq = seq.forge(includetime=True, **forger_kwargs) 87 | 88 | return forged_seq 89 | 90 | 91 | def _plot_summariser(seq: Dict[int, Dict]) -> Dict[int, Dict[str, np.ndarray]]: 92 | """ 93 | Return a plotting summary of a subsequence. 94 | 95 | Args: 96 | seq: The 'content' value of a forged sequence where a 97 | subsequence resides 98 | 99 | Returns: 100 | A dict that looks like a forged element, but all waveforms 101 | are just two points, np.array([min, max]) 102 | """ 103 | 104 | output = {} 105 | 106 | # we assume correctness, all postions specify the same channels 107 | chans = seq[1]['data'].keys() 108 | 109 | minmax = dict(zip(chans, [(0, 0)]*len(chans))) 110 | 111 | for element in seq.values(): 112 | 113 | arr_dict = element['data'] 114 | 115 | for chan in chans: 116 | wfm = arr_dict[chan]['wfm'] 117 | if wfm.min() < minmax[chan][0]: 118 | minmax[chan] = (wfm.min(), minmax[chan][1]) 119 | if wfm.max() > minmax[chan][1]: 120 | minmax[chan] = (minmax[chan][0], wfm.max()) 121 | output[chan] = {'wfm': np.array(minmax[chan]), 122 | 'm1': np.zeros(2), 123 | 'm2': np.zeros(2), 124 | 'time': np.linspace(0, 1, 2)} 125 | 126 | return output 127 | 128 | 129 | # the Grand Unified Plotter 130 | def plotter(obj_to_plot: BBObject, **forger_kwargs) -> None: 131 | """ 132 | The one plot function to be called. Turns whatever it gets 133 | into a sequence, forges it, and plots that. 134 | """ 135 | 136 | # TODO: Take axes as input 137 | 138 | # strategy: 139 | # * Validate 140 | # * Forge 141 | # * Plot 142 | 143 | _plot_object_validator(obj_to_plot) 144 | 145 | seq = _plot_object_forger(obj_to_plot, **forger_kwargs) 146 | 147 | # Get the dimensions. 148 | chans = seq[1]['content'][1]['data'].keys() 149 | seqlen = len(seq.keys()) 150 | 151 | def update_minmax(chanminmax, wfmdata, chanind): 152 | (thismin, thismax) = (wfmdata.min(), wfmdata.max()) 153 | if thismin < chanminmax[chanind][0]: 154 | chanminmax[chanind] = [thismin, chanminmax[chanind][1]] 155 | if thismax > chanminmax[chanind][1]: 156 | chanminmax[chanind] = [chanminmax[chanind][0], thismax] 157 | return chanminmax 158 | 159 | # Then figure out the figure scalings 160 | minf: float = -np.inf 161 | inf: float = np.inf 162 | chanminmax: List[Tuple[float, float]] = [(inf, minf)]*len(chans) 163 | for chanind, chan in enumerate(chans): 164 | for pos in range(1, seqlen+1): 165 | if seq[pos]['type'] == 'element': 166 | wfmdata = (seq[pos]['content'][1] 167 | ['data'][chan]['wfm']) 168 | chanminmax = update_minmax(chanminmax, wfmdata, chanind) 169 | elif seq[pos]['type'] == 'subsequence': 170 | for pos2 in seq[pos]['content'].keys(): 171 | elem = seq[pos]['content'][pos2]['data'] 172 | wfmdata = elem[chan]['wfm'] 173 | chanminmax = update_minmax(chanminmax, 174 | wfmdata, chanind) 175 | 176 | fig, axs = plt.subplots(len(chans), seqlen) 177 | 178 | # ...and do the plotting 179 | for chanind, chan in enumerate(chans): 180 | 181 | # figure out the channel voltage scaling 182 | # The entire channel shares a y-axis 183 | 184 | minmax: Tuple[float, float] = chanminmax[chanind] 185 | 186 | (voltagescaling, voltageprefix) = getSIScalingAndPrefix(minmax) 187 | voltageunit = voltageprefix + 'V' 188 | 189 | for pos in range(seqlen): 190 | # 1 by N arrays are indexed differently than M by N arrays 191 | # and 1 by 1 arrays are not arrays at all... 192 | if len(chans) == 1 and seqlen > 1: 193 | ax = axs[pos] 194 | if len(chans) > 1 and seqlen == 1: 195 | ax = axs[chanind] 196 | if len(chans) == 1 and seqlen == 1: 197 | ax = axs 198 | if len(chans) > 1 and seqlen > 1: 199 | ax = axs[chanind, pos] 200 | 201 | # reduce the tickmark density (must be called before scaling) 202 | ax.locator_params(tight=True, nbins=4, prune='lower') 203 | 204 | if seq[pos+1]['type'] == 'element': 205 | content = seq[pos+1]['content'][1]['data'][chan] 206 | wfm = content['wfm'] 207 | m1 = content.get('m1', np.zeros_like(wfm)) 208 | m2 = content.get('m2', np.zeros_like(wfm)) 209 | time = content['time'] 210 | newdurs = content.get('newdurations', []) 211 | 212 | else: 213 | arr_dict = _plot_summariser(seq[pos+1]['content']) 214 | wfm = arr_dict[chan]['wfm'] 215 | newdurs = [] 216 | 217 | ax.annotate('SUBSEQ', xy=(0.5, 0.5), 218 | xycoords='axes fraction', 219 | horizontalalignment='center') 220 | time = np.linspace(0, 1, 2) # needed for timeexponent 221 | 222 | # Figure out the axes' scaling 223 | timeexponent = np.log10(time.max()) 224 | timeunit = 's' 225 | timescaling: float = 1.0 226 | if timeexponent < 0: 227 | timeunit = 'ms' 228 | timescaling = 1e3 229 | if timeexponent < -3: 230 | timeunit = 'micro s' 231 | timescaling = 1e6 232 | if timeexponent < -6: 233 | timeunit = 'ns' 234 | timescaling = 1e9 235 | 236 | if seq[pos+1]['type'] == 'element': 237 | ax.plot(timescaling*time, voltagescaling*wfm, lw=3, 238 | color=(0.6, 0.4, 0.3), alpha=0.4) 239 | 240 | ymax = voltagescaling * chanminmax[chanind][1] 241 | ymin = voltagescaling * chanminmax[chanind][0] 242 | yrange = ymax - ymin 243 | ax.set_ylim([ymin-0.05*yrange, ymax+0.2*yrange]) 244 | 245 | if seq[pos+1]['type'] == 'element': 246 | # TODO: make this work for more than two markers 247 | 248 | # marker1 (red, on top) 249 | y_m1 = ymax+0.15*yrange 250 | marker_on = np.ones_like(m1) 251 | marker_on[m1 == 0] = np.nan 252 | marker_off = np.ones_like(m1) 253 | ax.plot(timescaling*time, y_m1*marker_off, 254 | color=(0.6, 0.1, 0.1), alpha=0.2, lw=2) 255 | ax.plot(timescaling*time, y_m1*marker_on, 256 | color=(0.6, 0.1, 0.1), alpha=0.6, lw=2) 257 | 258 | # marker 2 (blue, below the red) 259 | y_m2 = ymax+0.10*yrange 260 | marker_on = np.ones_like(m2) 261 | marker_on[m2 == 0] = np.nan 262 | marker_off = np.ones_like(m2) 263 | ax.plot(timescaling*time, y_m2*marker_off, 264 | color=(0.1, 0.1, 0.6), alpha=0.2, lw=2) 265 | ax.plot(timescaling*time, y_m2*marker_on, 266 | color=(0.1, 0.1, 0.6), alpha=0.6, lw=2) 267 | 268 | # If subsequence, plot lines indicating min and max value 269 | if seq[pos+1]['type'] == 'subsequence': 270 | # min: 271 | ax.plot(time, np.ones_like(time)*wfm[0], 272 | color=(0.12, 0.12, 0.12), alpha=0.2, lw=2) 273 | # max: 274 | ax.plot(time, np.ones_like(time)*wfm[1], 275 | color=(0.12, 0.12, 0.12), alpha=0.2, lw=2) 276 | 277 | ax.set_xticks([]) 278 | 279 | # time step lines 280 | for dur in np.cumsum(newdurs): 281 | ax.plot([timescaling*dur, timescaling*dur], 282 | [ax.get_ylim()[0], ax.get_ylim()[1]], 283 | color=(0.312, 0.2, 0.33), 284 | alpha=0.3) 285 | 286 | # labels 287 | if pos == 0: 288 | ax.set_ylabel('({})'.format(voltageunit)) 289 | if pos == seqlen - 1 and not(isinstance(obj_to_plot, BluePrint)): 290 | newax = ax.twinx() 291 | newax.set_yticks([]) 292 | if isinstance(chan, int): 293 | new_ylabel = f'Ch. {chan}' 294 | elif isinstance(chan, str): 295 | new_ylabel = chan 296 | newax.set_ylabel(new_ylabel) 297 | 298 | if seq[pos+1]['type'] == 'subsequence': 299 | ax.set_xlabel('Time N/A') 300 | else: 301 | ax.set_xlabel('({})'.format(timeunit)) 302 | 303 | # remove excess space from the plot 304 | if not chanind+1 == len(chans): 305 | ax.set_xticks([]) 306 | if not pos == 0: 307 | ax.set_yticks([]) 308 | fig.subplots_adjust(hspace=0, wspace=0) 309 | 310 | # display sequencer information 311 | if chanind == 0 and isinstance(obj_to_plot, Sequence): 312 | seq_info = seq[pos+1]['sequencing'] 313 | titlestring = '' 314 | if seq_info['twait'] == 1: # trigger wait 315 | titlestring += 'T ' 316 | if seq_info['nrep'] > 1: # nreps 317 | titlestring += '\u21BB{} '.format(seq_info['nrep']) 318 | if seq_info['nrep'] == 0: 319 | titlestring += '\u221E ' 320 | if seq_info['jump_input'] != 0: 321 | if seq_info['jump_input'] == -1: 322 | titlestring += 'E\u2192 ' 323 | else: 324 | titlestring += 'E{} '.format(seq_info['jump_input']) 325 | if seq_info['goto'] > 0: 326 | titlestring += '\u21b1{}'.format(seq_info['goto']) 327 | 328 | ax.set_title(titlestring) 329 | -------------------------------------------------------------------------------- /tests/test_sequence.py: -------------------------------------------------------------------------------- 1 | # Test suite for the Sequence Object of the broadband package 2 | # 3 | # Horribly unfinished. 4 | # 5 | # The strategy is the same as for the BluePrint test suite: we cook up some 6 | # sequences and try to break them. If we can't, everything is prolly OK 7 | 8 | import pytest 9 | import broadbean as bb 10 | from broadbean.sequence import (SequenceCompatibilityError, 11 | SequenceConsistencyError, Sequence) 12 | from broadbean.tools import makeVaryingSequence, repeatAndVarySequence 13 | 14 | ramp = bb.PulseAtoms.ramp 15 | sine = bb.PulseAtoms.sine 16 | 17 | 18 | @pytest.fixture 19 | def protosequence1(): 20 | 21 | SR = 1e9 22 | 23 | th = bb.BluePrint() 24 | th.insertSegment(0, ramp, args=(0, 0), name='ramp', dur=10e-6) 25 | th.insertSegment(1, ramp, args=(1, 1), name='ramp', dur=5e-6) 26 | th.insertSegment(2, ramp, args=(0, 0), name='ramp', dur=10e-6) 27 | th.setSR(SR) 28 | 29 | wiggle1 = bb.BluePrint() 30 | wiggle1.insertSegment(0, sine, args=(4e6, 0.5, 0), dur=25e-6) 31 | wiggle1.setSR(SR) 32 | 33 | wiggle2 = bb.BluePrint() 34 | wiggle2.insertSegment(0, sine, args=(8e6, 0.5, 0), dur=25e-6) 35 | wiggle2.setSR(SR) 36 | 37 | elem1 = bb.Element() 38 | elem1.addBluePrint(1, th) 39 | elem1.addBluePrint(2, wiggle1) 40 | 41 | elem2 = bb.Element() 42 | elem2.addBluePrint(1, th) 43 | elem2.addBluePrint(2, wiggle2) 44 | 45 | seq = Sequence() 46 | seq.addElement(1, elem1) 47 | seq.addElement(2, elem2) 48 | seq.setSR(SR) 49 | 50 | seq.setChannelAmplitude(1, 2) 51 | seq.setChannelOffset(1, 0) 52 | seq.setChannelAmplitude(2, 2) 53 | seq.setChannelOffset(2, 0) 54 | seq.setSequencingTriggerWait(1, 1) 55 | seq.setSequencingEventJumpTarget(1, 1) 56 | seq.setSequencingGoto(1, 1) 57 | seq.setSequencingTriggerWait(2, 1) 58 | seq.setSequencingEventJumpTarget(2, 1) 59 | seq.setSequencingGoto(2, 1) 60 | 61 | return seq 62 | 63 | 64 | @pytest.fixture 65 | def protosequence2(): 66 | 67 | SR = 1e9 68 | 69 | saw = bb.BluePrint() 70 | saw.insertSegment(0, ramp, args=(0, 100e-3), dur=11e-6) 71 | saw.insertSegment(1, 'waituntil', args=(25e-6)) 72 | saw.setSR(SR) 73 | 74 | lineandwiggle = bb.BluePrint() 75 | lineandwiggle.insertSegment(0, 'waituntil', args=(11e-6)) 76 | lineandwiggle.insertSegment(1, sine, args=(10e6, 50e-6, 10e-6), dur=14e-6) 77 | lineandwiggle.setSR(SR) 78 | 79 | elem1 = bb.Element() 80 | elem1.addBluePrint(1, saw) 81 | elem1.addBluePrint(2, lineandwiggle) 82 | 83 | elem2 = bb.Element() 84 | elem2.addBluePrint(2, saw) 85 | elem2.addBluePrint(1, lineandwiggle) 86 | 87 | seq = Sequence() 88 | seq.setSR(SR) 89 | seq.addElement(1, elem1) 90 | seq.addElement(2, elem2) 91 | 92 | seq.setChannelAmplitude(1, 1.5) 93 | seq.setChannelOffset(1, 0) 94 | seq.setChannelAmplitude(2, 1) 95 | seq.setChannelOffset(2, 0) 96 | seq.setSequencingTriggerWait(1, 0) 97 | seq.setSequencingTriggerWait(2, 1) 98 | seq.setSequencingNumberOfRepetitions(1, 2) 99 | seq.setSequencingEventJumpTarget(1, 0) 100 | seq.setSequencingEventJumpTarget(2, 0) 101 | seq.setSequencingGoto(1, 2) 102 | seq.setSequencingGoto(2, 1) 103 | 104 | return seq 105 | 106 | 107 | @pytest.fixture 108 | def badseq_missing_pos(): 109 | 110 | SR = 1e9 111 | 112 | saw = bb.BluePrint() 113 | saw.insertSegment(0, ramp, args=(0, 100e-3), dur=11e-6) 114 | saw.insertSegment(1, 'waituntil', args=(25e-6)) 115 | saw.setSR(SR) 116 | 117 | lineandwiggle = bb.BluePrint() 118 | lineandwiggle.insertSegment(0, 'waituntil', args=(11e-6)) 119 | lineandwiggle.insertSegment(1, sine, args=(10e6, 50e-6, 10e-6), dur=14e-6) 120 | lineandwiggle.setSR(SR) 121 | 122 | elem1 = bb.Element() 123 | elem1.addBluePrint(1, saw) 124 | elem1.addBluePrint(2, lineandwiggle) 125 | 126 | elem2 = bb.Element() 127 | elem2.addBluePrint(2, saw) 128 | elem2.addBluePrint(1, lineandwiggle) 129 | 130 | seq = Sequence() 131 | seq.setSR(SR) 132 | seq.addElement(1, elem1) 133 | seq.addElement(3, elem2) # <--- A gap in the sequence 134 | 135 | seq.setChannelAmplitude(1, 1.5) 136 | seq.setChannelOffset(1, 0) 137 | seq.setChannelAmplitude(2, 1) 138 | seq.setChannelOffset(2, 0) 139 | seq.setSequencingTriggerWait(3, 1) 140 | seq.setSequencingNumberOfRepetitions(1, 2) 141 | seq.setSequencingGoto(1, 2) 142 | seq.setSequencingGoto(3, 1) 143 | # seq.setSequenceSettings(1, 0, 2, 0, 2) 144 | # seq.setSequenceSettings(2, 1, 1, 0, 1) 145 | 146 | return seq 147 | 148 | 149 | @pytest.fixture 150 | def squarepulse_baseelem(): 151 | 152 | SR = 1e6 153 | 154 | basebp = bb.BluePrint() 155 | basebp.insertSegment(0, ramp, (0, 0), dur=0.5e-4) 156 | basebp.insertSegment(1, ramp, (1, 1), dur=1e-4, name='varyme') 157 | basebp.insertSegment(2, 'waituntil', 5e-4) 158 | basebp.setSR(SR) 159 | 160 | baseelem = bb.Element() 161 | baseelem.addBluePrint(1, basebp) 162 | 163 | return baseelem 164 | 165 | 166 | ################################################## 167 | # INIT and dunderdunder part 168 | 169 | 170 | @pytest.mark.parametrize('attribute', [('_data'), ('_sequencing'), 171 | ('_awgspecs'), ('_meta')]) 172 | def test_copy_positively(protosequence1, attribute): 173 | new_seq = protosequence1.copy() 174 | attr1 = new_seq.__getattribute__(attribute) 175 | attr2 = protosequence1.__getattribute__(attribute) 176 | assert attr1 == attr2 177 | 178 | 179 | def test_copy_negatively_01(protosequence1): 180 | new_seq = protosequence1.copy() 181 | new_seq.setSequencingTriggerWait(1, 0) 182 | new_seq.setSequencingNumberOfRepetitions(1, 1) 183 | new_seq.setSequencingEventJumpTarget(1, 1) 184 | new_seq.setSequencingGoto(1, 1) 185 | 186 | assert new_seq != protosequence1 187 | 188 | 189 | def test_copy_negatively_02(protosequence1): 190 | new_seq = protosequence1.copy() 191 | new_seq.setChannelAmplitude(1, 1.9) 192 | assert new_seq != protosequence1 193 | 194 | 195 | def test_copy_negatively_03(protosequence1): 196 | new_seq = protosequence1.copy() 197 | new_seq.element(1).changeArg(2, 'sine', 'freq', 1e6) 198 | assert new_seq != protosequence1 199 | 200 | 201 | def test_copy_and_eq(protosequence1): 202 | new_seq = protosequence1.copy() 203 | assert new_seq == protosequence1 204 | 205 | 206 | def test_addition_fail_vrange(protosequence1, protosequence2): 207 | with pytest.raises(SequenceCompatibilityError): 208 | protosequence1 + protosequence2 209 | 210 | 211 | def test_addition_fail_position(protosequence1, badseq_missing_pos): 212 | with pytest.raises(SequenceConsistencyError): 213 | protosequence1 + badseq_missing_pos 214 | 215 | 216 | def test_addition_data(protosequence1, protosequence2): 217 | protosequence2.setChannelAmplitude(1, 2) 218 | protosequence2.setChannelOffset(1, 0) 219 | protosequence2.setChannelAmplitude(2, 2) 220 | protosequence2.setChannelOffset(2, 0) 221 | newseq = protosequence1 + protosequence2 222 | expected_data = {1: protosequence1.element(1), 223 | 2: protosequence1.element(2), 224 | 3: protosequence2.element(1), 225 | 4: protosequence2.element(2)} 226 | assert newseq._data == expected_data 227 | 228 | 229 | def test_addition_sequencing1(protosequence1, protosequence2): 230 | protosequence2.setChannelAmplitude(1, 2) 231 | protosequence2.setChannelOffset(1, 0) 232 | protosequence2.setChannelAmplitude(2, 2) 233 | protosequence2.setChannelOffset(2, 0) 234 | 235 | newseq = protosequence1 + protosequence2 236 | expected_sequencing = {1: {'twait': 1, 'nrep': 1, 'jump_target': 1, 237 | 'goto': 1, 'jump_input': 0}, 238 | 2: {'twait': 1, 'nrep': 1, 'jump_target': 1, 239 | 'goto': 1, 'jump_input': 0}, 240 | 3: {'twait': 0, 'nrep': 2, 'jump_target': 0, 241 | 'goto': 4, 'jump_input': 0}, 242 | 4: {'twait': 1, 'nrep': 1, 'jump_target': 0, 243 | 'goto': 3, 'jump_input': 0}} 244 | assert newseq._sequencing == expected_sequencing 245 | 246 | 247 | def test_addition_sequencing2(protosequence1, protosequence2): 248 | protosequence2.setChannelAmplitude(1, 2) 249 | protosequence2.setChannelOffset(1, 0) 250 | protosequence2.setChannelAmplitude(2, 2) 251 | protosequence2.setChannelOffset(2, 0) 252 | 253 | newseq = protosequence2 + protosequence1 254 | expected_sequencing = {3: {'twait': 1, 'nrep': 1, 'jump_target': 3, 255 | 'goto': 3, 'jump_input': 0}, 256 | 4: {'twait': 1, 'nrep': 1, 'jump_target': 3, 257 | 'goto': 3, 'jump_input': 0}, 258 | 1: {'twait': 0, 'nrep': 2, 'jump_target': 0, 259 | 'goto': 2, 'jump_input': 0}, 260 | 2: {'twait': 1, 'nrep': 1, 'jump_target': 0, 261 | 'goto': 1, 'jump_input': 0}} 262 | assert newseq._sequencing == expected_sequencing 263 | 264 | 265 | def test_addition_awgspecs(protosequence1, protosequence2): 266 | protosequence2.setChannelAmplitude(1, 2) 267 | protosequence2.setChannelOffset(1, 0) 268 | protosequence2.setChannelAmplitude(2, 2) 269 | protosequence2.setChannelOffset(2, 0) 270 | 271 | newseq = protosequence1 + protosequence2 272 | 273 | assert newseq._awgspecs == protosequence1._awgspecs 274 | 275 | 276 | def test_addition_data_with_empty(protosequence1): 277 | newseq = Sequence() 278 | newseq._awgspecs = protosequence1._awgspecs 279 | 280 | newseq = newseq + protosequence1 281 | 282 | assert newseq._data == protosequence1._data 283 | 284 | 285 | def test_add_subsequence_raises(protosequence1, squarepulse_baseelem): 286 | 287 | # raise if a non-Sequence object is added 288 | with pytest.raises(ValueError): 289 | protosequence1.addSubSequence(1, squarepulse_baseelem) 290 | 291 | seq = Sequence() 292 | seq.addElement(1, squarepulse_baseelem) 293 | seq.setSR(squarepulse_baseelem.SR) 294 | 295 | mainseq = Sequence() 296 | mainseq.setSR(seq.SR/2) 297 | 298 | # raise if the subsequence sample rate does not match the main seq. SR 299 | with pytest.raises(ValueError): 300 | mainseq.addSubSequence(1, seq) 301 | 302 | mainseq.setSR(seq.SR) 303 | mainseq.addSubSequence(1, seq) 304 | 305 | doublemainseq = Sequence() 306 | doublemainseq.setSR(seq.SR) 307 | 308 | with pytest.raises(ValueError): 309 | doublemainseq.addSubSequence(1, mainseq) 310 | 311 | ################################################## 312 | # AWG settings 313 | 314 | 315 | def test_setSR(protosequence1): 316 | protosequence1.setSR(1.2e9) 317 | assert protosequence1._awgspecs['SR'] == 1.2e9 318 | 319 | 320 | ################################################## 321 | # Highest level sequence variers 322 | 323 | @pytest.mark.parametrize('channels, names, args, iters', 324 | [([1], ['varyme'], ['start', 'stop'], [0.9, 1.0, 1.1]), 325 | ([1, 1], ['varyme', 'ramp'], ['start', 'start'], [(1,), (1,2)]), 326 | ([1], ['varyme'], ['crazyarg'], [0.9, 1.0, 1.1])]) 327 | def test_makeVaryingSequence_fail(squarepulse_baseelem, channels, names, 328 | args, iters): 329 | with pytest.raises(ValueError): 330 | makeVaryingSequence(squarepulse_baseelem, channels, 331 | names, args, iters) 332 | 333 | 334 | @pytest.mark.parametrize('seqpos, argslist', [(1, [(0, 0), 2*(1,), (5e-4,)]), 335 | (2, [(0, 0), 2*(1.2,), (5e-4,)]), 336 | (3, [(0, 0), 2*(1.3,), (5e-4,)])]) 337 | def test_makeVaryingSequence(squarepulse_baseelem, seqpos, argslist): 338 | channels = [1, 1] 339 | names = ['varyme', 'varyme'] 340 | args = ['start', 'stop'] 341 | iters = 2*[[1, 1.2, 1.3]] 342 | sequence = makeVaryingSequence(squarepulse_baseelem, channels, 343 | names, args, iters) 344 | assert sequence._data[seqpos]._data[1]['blueprint']._argslist == argslist 345 | 346 | 347 | def test_repeatAndVarySequence_length(protosequence1): 348 | poss = [1] 349 | channels = [1] 350 | names = ['ramp'] 351 | args = ['start'] 352 | iters = [[1, 1.1, 1.2]] 353 | 354 | newseq = repeatAndVarySequence(protosequence1, poss, channels, names, 355 | args, iters) 356 | 357 | expected_l = len(iters[0])*protosequence1.length_sequenceelements 358 | 359 | assert newseq.length_sequenceelements == expected_l 360 | 361 | 362 | def test_repeatAndVarySequence_awgspecs(protosequence1): 363 | poss = (1,) 364 | channels = [1] 365 | names = ['ramp'] 366 | args = ['stop'] 367 | iters = [[1, 0.9, 0.8]] 368 | 369 | newseq = repeatAndVarySequence(protosequence1, poss, channels, names, 370 | args, iters) 371 | 372 | assert newseq._awgspecs == protosequence1._awgspecs 373 | 374 | 375 | def test_repeatAndVarySequence_fail_inputlength1(protosequence1): 376 | poss = (1, 2) 377 | channels = [1] 378 | names = ['ramp'] 379 | args = ['start'] 380 | iters = [(1, 0.2, 0.3)] 381 | 382 | with pytest.raises(ValueError): 383 | repeatAndVarySequence(protosequence1, poss, 384 | channels, names, args, iters) 385 | 386 | 387 | def test_repeatAndVarySequence_fail_inputlength2(protosequence1): 388 | poss = (1, 2) 389 | channels = [1, 1] 390 | names = ['ramp', 'ramp'] 391 | args = ['start', 'stop'] 392 | iters = [(1, 0.2, 0.3), (1, 0.2)] 393 | 394 | with pytest.raises(ValueError): 395 | repeatAndVarySequence(protosequence1, poss, 396 | channels, names, args, iters) 397 | 398 | 399 | def test_repeatAndVarySequence_fail_consistency(protosequence1, 400 | squarepulse_baseelem): 401 | 402 | protosequence1.addElement(5, squarepulse_baseelem) 403 | 404 | print(protosequence1.checkConsistency()) 405 | 406 | poss = (1,) 407 | channels = [1] 408 | names = ['ramp'] 409 | args = ['start'] 410 | iters = [(1, 0.2, 0.3)] 411 | 412 | with pytest.raises(SequenceConsistencyError): 413 | repeatAndVarySequence(protosequence1, poss, 414 | channels, names, args, iters) 415 | 416 | 417 | @pytest.mark.parametrize('pos', [2, 4, 6]) 418 | def test_repeatAndVarySequence_same_elements(protosequence1, pos): 419 | poss = (1,) 420 | channels = [1] 421 | names = ['ramp'] 422 | args = ['start'] 423 | iters = [(1, 0.2, 0.3)] 424 | 425 | newseq = repeatAndVarySequence(protosequence1, poss, channels, 426 | names, args, iters) 427 | assert newseq.element(pos) == protosequence1.element(2) 428 | -------------------------------------------------------------------------------- /tests/test_blueprint.py: -------------------------------------------------------------------------------- 1 | # Test suite for the BluePrint Object of the broadband package 2 | # 3 | # It is impossible to make a complete test of all the many possible blueprints. 4 | # The strategy is to make a few representative (we hope!) blueprints and then 5 | # assert that hitting them with all their methods results in new blueprints 6 | # with the desired data attributes (the desired state) 7 | 8 | import pytest 9 | import broadbean as bb 10 | 11 | ramp = bb.PulseAtoms.ramp 12 | sine = bb.PulseAtoms.sine 13 | 14 | tophat_SR = 2000 15 | 16 | 17 | @pytest.fixture 18 | def virgin_blueprint(): 19 | """ 20 | Return an empty instance of BluePrint 21 | """ 22 | return bb.BluePrint() 23 | 24 | 25 | @pytest.fixture 26 | def blueprint_tophat(): 27 | """ 28 | Return a blueprint consisting of three slopeless ramps forming something 29 | similar to a tophat 30 | """ 31 | th = bb.BluePrint() 32 | th.insertSegment(0, ramp, args=(0, 0), name='ramp', dur=1) 33 | th.insertSegment(1, ramp, args=(1, 1), name='ramp', dur=0.5) 34 | th.insertSegment(2, ramp, args=(0, 0), name='ramp', dur=1) 35 | th.setSR(tophat_SR) 36 | 37 | return th 38 | 39 | 40 | @pytest.fixture 41 | def blueprint_nasty(): 42 | """ 43 | Return a nasty blueprint trying to hit some corner cases 44 | """ 45 | ns = bb.BluePrint() 46 | ns.insertSegment(0, 'waituntil', args=(1,)) 47 | ns.insertSegment(1, ramp, (-1/3, 1/3), dur=0.1) 48 | ns.insertSegment(2, 'waituntil', args=(1+2/3,)) 49 | ns.setSR(tophat_SR) 50 | 51 | ns.setSegmentMarker('ramp', (-0.1, 0.1), 1) 52 | ns.setSegmentMarker('waituntil2', (0, 2/3), 2) 53 | 54 | ns.marker1 = [(0, 0.1)] 55 | ns.marker2 = [(1, 0.1)] 56 | 57 | return ns 58 | 59 | ################################################## 60 | # TEST STATIC METHODS 61 | 62 | 63 | @pytest.mark.parametrize('inp, outp', [('', ''), 64 | ('test', 'test'), 65 | ('test3', 'test'), 66 | ('2test3', '2test'), 67 | ('123_', '123_'), 68 | ('123_4', '123_')]) 69 | def test_basename(inp, outp): 70 | assert bb.BluePrint._basename(inp) == outp 71 | 72 | 73 | @pytest.mark.parametrize('notstring', [(1, 1.1, [], (), 74 | ('name1',), 75 | ['name2'])]) 76 | def test_basename_input(notstring): 77 | with pytest.raises(ValueError): 78 | bb.BluePrint._basename(notstring) 79 | 80 | 81 | namelistsinout = [(['name', 'name'], ['name', 'name2']), 82 | (['ramp', 'sine', 'ramp', 'sine'], 83 | ['ramp', 'sine', 'ramp2', 'sine2']), 84 | (['name2', 'name'], ['name', 'name2']), 85 | (['n3', 'n2', 'n1'], ['n', 'n2', 'n3']), 86 | (['n', '2n', 'ss1', 'ss3'], ['n', '2n', 'ss', 'ss2'])] 87 | 88 | 89 | def test_make_names_unique0(): 90 | inp = namelistsinout[0][0] 91 | outp = namelistsinout[0][1] 92 | assert bb.BluePrint._make_names_unique(inp) == outp 93 | 94 | 95 | def test_make_names_unique1(): 96 | inp = namelistsinout[1][0] 97 | outp = namelistsinout[1][1] 98 | assert bb.BluePrint._make_names_unique(inp) == outp 99 | 100 | 101 | def test_make_names_unique2(): 102 | inp = namelistsinout[2][0] 103 | outp = namelistsinout[2][1] 104 | assert bb.BluePrint._make_names_unique(inp) == outp 105 | 106 | 107 | def test_make_names_unique3(): 108 | inp = namelistsinout[3][0] 109 | outp = namelistsinout[3][1] 110 | assert bb.BluePrint._make_names_unique(inp) == outp 111 | 112 | 113 | def test_make_names_unique4(): 114 | inp = namelistsinout[4][0] 115 | outp = namelistsinout[4][1] 116 | assert bb.BluePrint._make_names_unique(inp) == outp 117 | 118 | 119 | def test_make_names_unique_input(): 120 | with pytest.raises(ValueError): 121 | bb.BluePrint._make_names_unique('name') 122 | 123 | ################################################## 124 | # TEST BARE INITIALISATION 125 | 126 | 127 | def test_creation(virgin_blueprint): 128 | assert isinstance(virgin_blueprint, bb.BluePrint) 129 | 130 | 131 | @pytest.mark.parametrize("attribute, expected", [('_funlist', []), 132 | ('_namelist', []), 133 | ('_argslist', []), 134 | ('_durslist', []), 135 | ('marker1', []), 136 | ('marker2', []), 137 | ('_segmark1', []), 138 | ('_segmark2', [])]) 139 | def test_bare_init(virgin_blueprint, attribute, expected): 140 | assert virgin_blueprint.__getattribute__(attribute) == expected 141 | 142 | ################################################## 143 | # TEST WITH TOPHAT 144 | 145 | 146 | @pytest.mark.parametrize("attribute, val", [('_funlist', [ramp, ramp, ramp]), 147 | ('_namelist', 148 | ['ramp', 'ramp2', 'ramp3']), 149 | ('_argslist', 150 | [(0, 0), (1, 1), (0, 0)]), 151 | ('_durslist', 152 | [1, 0.5, 1]), 153 | ('marker1', []), 154 | ('marker2', []), 155 | ('_segmark1', [(0, 0)]*3), 156 | ('_segmark2', [(0, 0)]*3)]) 157 | def test_tophat_init(blueprint_tophat, attribute, val): 158 | assert blueprint_tophat.__getattribute__(attribute) == val 159 | 160 | 161 | @pytest.mark.parametrize("attribute, val", [('_funlist', [ramp, ramp, ramp]), 162 | ('_namelist', 163 | ['ramp', 'ramp2', 'ramp3']), 164 | ('_argslist', 165 | [(0, 0), (1, 1), (0, 0)]), 166 | ('_durslist', 167 | [1, 0.5, 1]), 168 | ('marker1', []), 169 | ('marker2', []), 170 | ('_segmark1', [(0, 0)]*3), 171 | ('_segmark2', [(0, 0)]*3)]) 172 | def test_tophat_copy(blueprint_tophat, attribute, val): 173 | new_bp = blueprint_tophat.copy() 174 | assert new_bp.__getattribute__(attribute) == val 175 | 176 | 177 | @pytest.mark.parametrize('name, newdur, durslist', 178 | [('ramp', 0.1, [0.1, 0.5, 1]), 179 | ('ramp2', 0.1, [1, 0.1, 1]), 180 | ('ramp3', 0.1, [1, 0.5, 0.1])]) 181 | def test_tophat_changeduration(blueprint_tophat, name, newdur, durslist): 182 | blueprint_tophat.changeDuration(name, newdur) 183 | assert blueprint_tophat._durslist == durslist 184 | 185 | 186 | def test_tophat_changeduration_everywhere(blueprint_tophat): 187 | blueprint_tophat.changeDuration('ramp', 0.2, replaceeverywhere=True) 188 | assert blueprint_tophat._durslist == [0.2]*3 189 | 190 | 191 | @pytest.mark.parametrize('newdur', [-1, 0.0, 1/(tophat_SR+1), None]) 192 | def test_tophat_changeduration_valueerror(blueprint_tophat, newdur): 193 | with pytest.raises(ValueError): 194 | blueprint_tophat.changeDuration('ramp', newdur) 195 | 196 | 197 | @pytest.mark.parametrize('name, arg, newval, argslist', 198 | [('ramp', 'start', -1, [(-1, 0), (1, 1), (0, 0)]), 199 | ('ramp', 'stop', -1, [(0, -1), (1, 1), (0, 0)]), 200 | ('ramp', 0, -1, [(-1, 0), (1, 1), (0, 0)]), 201 | ('ramp', 1, -1, [(0, -1), (1, 1), (0, 0)]), 202 | ('ramp2', 'stop', -1, [(0, 0), (1, -1), (0, 0)])]) 203 | def test_tophat_changeargument(blueprint_tophat, name, arg, newval, argslist): 204 | blueprint_tophat.changeArg(name, arg, newval) 205 | assert blueprint_tophat._argslist == argslist 206 | 207 | 208 | @pytest.mark.parametrize('name, arg, newval, argslist', 209 | [('ramp', 'start', -1, [(-1, 0), (-1, 1), (-1, 0)]), 210 | ('ramp', 'stop', -1, [(0, -1), (1, -1), (0, -1)]), 211 | ('ramp', 0, -1, [(-1, 0), (-1, 1), (-1, 0)]), 212 | ('ramp', 1, -1, [(0, -1), (1, -1), (0, -1)])]) 213 | def test_tophat_changeargument_replaceeverywhere(blueprint_tophat, name, 214 | arg, newval, argslist): 215 | blueprint_tophat.changeArg(name, arg, newval, replaceeverywhere=True) 216 | assert blueprint_tophat._argslist == argslist 217 | 218 | 219 | @pytest.mark.parametrize('name, arg', [('ramp', 'freq'), 220 | ('ramp', -1), 221 | ('ramp', 2), 222 | ('ramp2', ''), 223 | ('ramp4', 1)]) 224 | def test_tophat_changeargument_valueerror(blueprint_tophat, name, arg): 225 | with pytest.raises(ValueError): 226 | blueprint_tophat.changeArg(name, arg, 0) 227 | 228 | 229 | @pytest.mark.parametrize('pos, func, funlist', 230 | [(0, ramp, [ramp, ramp, ramp, ramp]), 231 | (-1, sine, [ramp, ramp, ramp, sine]), 232 | (2, sine, [ramp, ramp, sine, ramp]), 233 | (3, sine, [ramp, ramp, ramp, sine])]) 234 | def test_tophat_insert_funlist(blueprint_tophat, pos, func, funlist): 235 | blueprint_tophat.insertSegment(pos, func, args=(1, 0), dur=1) 236 | assert blueprint_tophat._funlist == funlist 237 | 238 | 239 | newargs = (5, 5) 240 | 241 | 242 | @pytest.mark.parametrize('pos, argslist', 243 | [(0, [newargs, (0, 0), (1, 1), (0, 0)]), 244 | (-1, [(0, 0), (1, 1), (0, 0), newargs]), 245 | (2, [(0, 0), (1, 1), newargs, (0, 0)])]) 246 | def test_tophat_insert_argslist(blueprint_tophat, pos, argslist): 247 | blueprint_tophat.insertSegment(pos, ramp, newargs, dur=1) 248 | assert blueprint_tophat._argslist == argslist 249 | 250 | 251 | @pytest.mark.parametrize('pos, name, namelist', 252 | [(0, 'myramp', ['myramp', 'ramp', 'ramp2', 'ramp3']), 253 | (-1, 'myramp', ['ramp', 'ramp2', 'ramp3', 'myramp']), 254 | (2, 'ramp', ['ramp', 'ramp2', 'ramp3', 'ramp4'])]) 255 | def test_tophat_insert_namelist(blueprint_tophat, pos, name, namelist): 256 | blueprint_tophat.insertSegment(pos, ramp, newargs, name=name, dur=0.5) 257 | assert blueprint_tophat._namelist == namelist 258 | 259 | 260 | @pytest.mark.parametrize('name', ['ramp', 'ramp2', 'ramp3', 'ramp4']) 261 | def test_tophat_remove_namelist(blueprint_tophat, name): 262 | if name in blueprint_tophat._namelist: 263 | blueprint_tophat.removeSegment(name) 264 | assert blueprint_tophat._namelist == ['ramp', 'ramp2'] 265 | else: 266 | with pytest.raises(KeyError): 267 | blueprint_tophat.removeSegment(name) 268 | 269 | 270 | def test_tophat_remove_segmentmarker(blueprint_tophat): 271 | 272 | with pytest.raises(ValueError): 273 | blueprint_tophat.removeSegmentMarker('ramp', 3) 274 | with pytest.raises(ValueError): 275 | blueprint_tophat.removeSegmentMarker('no such name', 3) 276 | 277 | # Adding and removing should be equivalent to NOOP 278 | bpc = blueprint_tophat.copy() 279 | blueprint_tophat.setSegmentMarker('ramp', (0, 0.1), 1) 280 | blueprint_tophat.removeSegmentMarker('ramp', 1) 281 | assert bpc == blueprint_tophat 282 | 283 | with pytest.raises(KeyError): 284 | bpc.removeSegmentMarker('no such name', 1) 285 | 286 | 287 | ################################################## 288 | # DUNDERDUNDER 289 | 290 | 291 | def test_not_equal(blueprint_tophat): 292 | bpc = blueprint_tophat.copy() 293 | 294 | with pytest.raises(ValueError): 295 | bpc == '1' 296 | 297 | bpc.insertSegment(0, ramp, (0, 0), dur=1/3) 298 | 299 | assert (bpc == blueprint_tophat) is False 300 | 301 | bpc = blueprint_tophat.copy() 302 | blueprint_tophat.marker1 = [(0, 0.1)] 303 | bpc.marker2 = [(0, 0.1)] 304 | 305 | assert (bpc == blueprint_tophat) is False 306 | 307 | bpc = blueprint_tophat.copy() 308 | bpc.setSegmentMarker('ramp', (0, 0.1), 1) 309 | 310 | assert (bpc == blueprint_tophat) is False 311 | 312 | bpc = blueprint_tophat.copy() 313 | bpc.marker2 = [(0, 0.1)] 314 | 315 | assert (bpc == blueprint_tophat) is False 316 | 317 | bpc = blueprint_tophat.copy() 318 | blueprint_tophat.setSegmentMarker('ramp', (0, 0.1), 2) 319 | 320 | assert (bpc == blueprint_tophat) is False 321 | 322 | 323 | def test_add_two_identical(blueprint_tophat): 324 | bp = blueprint_tophat 325 | bpc = bp.copy() 326 | new_bp1 = bpc + bp 327 | new_bp2 = bp + bpc 328 | 329 | assert new_bp1 == new_bp2 330 | assert new_bp1._namelist == ['ramp', 'ramp2', 'ramp3', 'ramp4', 'ramp5', 331 | 'ramp6'] 332 | assert new_bp1._argslist == bp._argslist + bp._argslist 333 | assert new_bp1._funlist == bp._funlist + bp._funlist 334 | assert new_bp1._segmark1 == bp._segmark1 + bp._segmark1 335 | assert new_bp1._segmark2 == bp._segmark2 + bp._segmark2 336 | assert new_bp1._durslist == bp._durslist + bp._durslist 337 | assert new_bp1.marker1 == bp.marker1 + bp.marker1 338 | assert new_bp1.marker2 == bp.marker2 + bp.marker2 339 | 340 | 341 | def test_add_two_different(blueprint_tophat, blueprint_nasty): 342 | th = blueprint_tophat 343 | ns = blueprint_nasty 344 | 345 | bp = th + ns 346 | 347 | assert bp._namelist == ['ramp', 'ramp2', 'ramp3', 348 | 'waituntil', 'ramp4', 'waituntil2'] 349 | assert bp._argslist == th._argslist + ns._argslist 350 | assert bp._funlist == th._funlist + ns._funlist 351 | assert bp._segmark1 == th._segmark1 + ns._segmark1 352 | assert bp._segmark2 == th._segmark2 + ns._segmark2 353 | assert bp._durslist == th._durslist + ns._durslist 354 | assert bp.marker1 == th.marker1 + ns.marker1 355 | assert bp.marker2 == th.marker2 + ns.marker2 356 | 357 | 358 | ################################################## 359 | # MISC 360 | 361 | 362 | def test_description(blueprint_nasty, blueprint_tophat): 363 | desc1 = blueprint_nasty.description 364 | desc2 = blueprint_tophat.description 365 | 366 | exp_keys = ['marker1_abs', 'marker1_rel', 'marker2_abs', 'marker2_rel', 367 | 'segment_01', 'segment_02', 'segment_03'] 368 | 369 | assert sorted(list(desc1.keys())) == sorted(exp_keys) 370 | assert sorted(list(desc2.keys())) == sorted(exp_keys) 371 | 372 | # More to come... 373 | -------------------------------------------------------------------------------- /broadbean/element.py: -------------------------------------------------------------------------------- 1 | # This file contains the Element definition 2 | 3 | from typing import Union, Dict, List 4 | from copy import deepcopy 5 | 6 | import numpy as np 7 | 8 | from .broadbean import marked_for_deletion, PulseAtoms 9 | from broadbean.blueprint import BluePrint, _subelementBuilder 10 | 11 | 12 | class ElementDurationError(Exception): 13 | pass 14 | 15 | 16 | class Element: 17 | """ 18 | Object representing an element. An element is a collection of waves that 19 | are to be run simultaneously. The element consists of a number of channels 20 | that are then each filled with anything of the appropriate length. 21 | """ 22 | 23 | def __init__(self): 24 | 25 | # The internal data structure, a dict with key channel number 26 | # Each value is a dict with the following possible keys, values: 27 | # 'blueprint': a BluePrint 28 | # 'channelname': channel name for later use with a Tektronix AWG5014 29 | # 'array': a dict {'wfm': np.array} (other keys: 'm1', 'm2', etc) 30 | # 'SR': Sample rate. Used with array. 31 | # 32 | # Another dict is meta, which holds: 33 | # 'duration': duration in seconds of the entire element. 34 | # 'SR': sample rate of the element 35 | # These two values are added/updated upon validation of the durations 36 | 37 | self._data = {} 38 | self._meta = {} 39 | 40 | def addBluePrint(self, channel: Union[str, int], 41 | blueprint: BluePrint) -> None: 42 | """ 43 | Add a blueprint to the element on the specified channel. 44 | Overwrites whatever was there before. 45 | """ 46 | if not isinstance(blueprint, BluePrint): 47 | raise ValueError('Invalid blueprint given. Must be an instance' 48 | ' of the BluePrint class.') 49 | 50 | if [] in [blueprint._funlist, blueprint._argslist, blueprint._namelist, 51 | blueprint._durslist]: 52 | raise ValueError('Received empty BluePrint. Can not proceed.') 53 | 54 | # important: make a copy of the blueprint 55 | newprint = blueprint.copy() 56 | 57 | self._data[channel] = {} 58 | self._data[channel]['blueprint'] = newprint 59 | 60 | def addArray(self, channel: Union[int, str], waveform: np.ndarray, 61 | SR: int, **kwargs) -> None: 62 | """ 63 | Add an array of voltage value to the element on the specified channel. 64 | Overwrites whatever was there before. Markers can be specified via 65 | the kwargs, i.e. the kwargs must specify arrays of markers. The names 66 | can be 'm1', 'm2', 'm3', etc. 67 | 68 | Args: 69 | channel: The channel number 70 | waveform: The array of waveform values (V) 71 | SR: The sample rate in Sa/s 72 | """ 73 | 74 | N = len(waveform) 75 | self._data[channel] = {} 76 | self._data[channel]['array'] = {} 77 | 78 | for name, array in kwargs.items(): 79 | if len(array) != N: 80 | raise ValueError('Length mismatch between waveform and ' 81 | f'array {name}. Must be same length') 82 | self._data[channel]['array'].update({name: array}) 83 | 84 | self._data[channel]['array']['wfm'] = waveform 85 | self._data[channel]['SR'] = SR 86 | 87 | def validateDurations(self): 88 | """ 89 | Check that all channels have the same specified duration, number of 90 | points and sample rate. 91 | """ 92 | 93 | # pick out the channel entries 94 | channels = self._data.values() 95 | 96 | if len(channels) == 0: 97 | raise KeyError('Empty Element, nothing assigned') 98 | 99 | # First the sample rate 100 | SRs = [] 101 | for channel in channels: 102 | if 'blueprint' in channel.keys(): 103 | SRs.append(channel['blueprint'].SR) 104 | elif 'array' in channel.keys(): 105 | SR = channel['SR'] 106 | SRs.append(SR) 107 | 108 | if not SRs.count(SRs[0]) == len(SRs): 109 | errmssglst = zip(list(self._data.keys()), SRs) 110 | raise ElementDurationError('Different channels have different ' 111 | 'SRs. (Channel, SR): ' 112 | '{}'.format(list(errmssglst))) 113 | 114 | # Next the total time 115 | durations = [] 116 | for channel in channels: 117 | if 'blueprint' in channel.keys(): 118 | durations.append(channel['blueprint'].duration) 119 | elif 'array' in channel.keys(): 120 | length = len(channel['array']['wfm'])/channel['SR'] 121 | durations.append(length) 122 | 123 | if None not in SRs: 124 | atol = min(SRs) 125 | else: 126 | atol = 1e-9 127 | 128 | if not np.allclose(durations, durations[0], atol=atol): 129 | errmssglst = zip(list(self._data.keys()), durations) 130 | raise ElementDurationError('Different channels have different ' 131 | 'durations. (Channel, duration): ' 132 | '{}s'.format(list(errmssglst))) 133 | 134 | # Finally the number of points 135 | # (kind of redundant if sample rate and duration match?) 136 | npts = [] 137 | for channel in channels: 138 | if 'blueprint' in channel.keys(): 139 | npts.append(channel['blueprint'].points) 140 | elif 'array' in channel.keys(): 141 | length = len(channel['array']['wfm']) 142 | npts.append(length) 143 | 144 | if not npts.count(npts[0]) == len(npts): 145 | errmssglst = zip(list(self._data.keys()), npts) 146 | raise ElementDurationError('Different channels have different ' 147 | 'npts. (Channel, npts): ' 148 | '{}'.format(list(errmssglst))) 149 | 150 | # If these three tests pass, we equip the dictionary with convenient 151 | # info used by Sequence 152 | self._meta['SR'] = SRs[0] 153 | self._meta['duration'] = durations[0] 154 | 155 | def getArrays(self, 156 | includetime: bool=False) -> Dict[int, Dict[str, np.ndarray]]: 157 | """ 158 | Return arrays of the element. Heavily used by the Sequence. 159 | 160 | Args: 161 | includetime: Whether to include time arrays. They will have the key 162 | 'time'. Time should be included when plotting, otherwise not. 163 | 164 | Returns: 165 | dict: 166 | Dictionary with channel numbers (ints) as keys and forged 167 | blueprints as values. A forged blueprint is a dict with 168 | the mandatory key 'wfm' and optional keys 'm1', 'm2', 'm3' (etc) 169 | and 'time'. 170 | 171 | """ 172 | 173 | outdict = {} 174 | for channel, signal in self._data.items(): 175 | if 'array' in signal.keys(): 176 | outdict[channel] = signal['array'] 177 | if includetime and 'time' not in signal['array'].keys(): 178 | N = len(signal['array']['wfm']) 179 | dur = N/signal['SR'] 180 | outdict[channel]['array']['time'] = np.linspace(0, dur, N) 181 | elif 'blueprint' in signal.keys(): 182 | bp = signal['blueprint'] 183 | durs = bp.durations 184 | SR = bp.SR 185 | forged_bp = _subelementBuilder(bp, SR, durs) 186 | outdict[channel] = forged_bp 187 | if not includetime: 188 | outdict[channel].pop('time') 189 | outdict[channel].pop('newdurations') 190 | # TODO: should the be a separate bool for newdurations? 191 | 192 | return outdict 193 | 194 | @property 195 | def SR(self): 196 | """ 197 | Returns the sample rate, if well-defined. Else raises 198 | an error about what went wrong. 199 | """ 200 | # Will either raise an error or set self._data['SR'] 201 | self.validateDurations() 202 | 203 | return self._meta['SR'] 204 | 205 | @property 206 | def points(self) -> int: 207 | """ 208 | Returns the number of points of each channel if that number is 209 | well-defined. Else an error is raised. 210 | """ 211 | self.validateDurations() 212 | 213 | # pick out what is on the channels 214 | channels = self._data.values() 215 | 216 | # if validateDurations did not raise an error, all channels 217 | # have the same number of points 218 | for chan in channels: 219 | 220 | if not ('array' in chan.keys() or 'blueprint' in chan.keys()): 221 | raise ValueError('Neither BluePrint nor array assigned to ' 222 | 'chan {}!'.format(chan)) 223 | if 'blueprint' in chan.keys(): 224 | return chan['blueprint'].points 225 | else: 226 | return len(chan['array']['wfm']) 227 | 228 | else: 229 | # this line is here to make mypy happy; this exception is 230 | # already raised by validateDurations 231 | raise KeyError('Empty Element, nothing assigned') 232 | 233 | @property 234 | def duration(self): 235 | """ 236 | Returns the duration in seconds of the element, if said duration is 237 | well-defined. Else raises an error. 238 | """ 239 | # Will either raise an error or set self._data['SR'] 240 | self.validateDurations() 241 | 242 | return self._meta['duration'] 243 | 244 | @property 245 | def channels(self): 246 | """ 247 | The channels that has something on them 248 | """ 249 | chans = [key for key in self._data.keys()] 250 | return chans 251 | 252 | @property 253 | def description(self): 254 | """ 255 | Returns a dict describing the element. 256 | """ 257 | desc = {} 258 | 259 | for key, val in self._data.items(): 260 | if 'blueprint' in val.keys(): 261 | desc[str(key)] = val['blueprint'].description 262 | elif 'array' in val.keys(): 263 | desc[str(key)] = 'array' 264 | 265 | return desc 266 | 267 | def changeArg(self, channel: Union[str, int], 268 | name: str, arg: Union[str, int], value: Union[int, float], 269 | replaceeverywhere: bool=False) -> None: 270 | """ 271 | Change the argument of a function of the blueprint on the specified 272 | channel. 273 | 274 | Args: 275 | channel: The channel where the blueprint sits. 276 | name: The name of the segment in which to change an argument 277 | arg: Either the position (int) or name (str) of 278 | the argument to change 279 | value: The new value of the argument 280 | replaceeverywhere: If True, the same argument is overwritten 281 | in ALL segments where the name matches. E.g. 'gaussian1' will 282 | match 'gaussian', 'gaussian2', etc. If False, only the segment 283 | with exact name match gets a replacement. 284 | 285 | Raises: 286 | ValueError: If the specified channel has no blueprint. 287 | ValueError: If the argument can not be matched (either the argument 288 | name does not match or the argument number is wrong). 289 | """ 290 | 291 | if channel not in self.channels: 292 | raise ValueError(f'Nothing assigned to channel {channel}') 293 | 294 | if 'blueprint' not in self._data[channel].keys(): 295 | raise ValueError('No blueprint on channel {}.'.format(channel)) 296 | 297 | bp = self._data[channel]['blueprint'] 298 | 299 | bp.changeArg(name, arg, value, replaceeverywhere) 300 | 301 | def changeDuration(self, channel: Union[str, int], name: str, 302 | newdur: Union[int, float], 303 | replaceeverywhere: bool=False) -> None: 304 | """ 305 | Change the duration of a segment of the blueprint on the specified 306 | channel 307 | 308 | Args: 309 | channel: The channel holding the blueprint in question 310 | name): The name of the segment to modify 311 | newdur: The new duration. 312 | replaceeverywhere: If True, all segments 313 | matching the base 314 | name given will have their duration changed. If False, only the 315 | segment with an exact name match will have its duration 316 | changed. Default: False. 317 | """ 318 | 319 | if channel not in self.channels: 320 | raise ValueError(f'Nothing assigned to channel {channel}') 321 | 322 | if 'blueprint' not in self._data[channel].keys(): 323 | raise ValueError('No blueprint on channel {}.'.format(channel)) 324 | 325 | bp = self._data[channel]['blueprint'] 326 | 327 | bp.changeDuration(name, newdur, replaceeverywhere) 328 | 329 | def _applyDelays(self, delays: List[float]) -> None: 330 | """ 331 | Apply delays to the channels of this element. This function is intended 332 | to be used via a Sequence object. Note that this function changes 333 | the element it is called on. Calling _applyDelays a second will apply 334 | more delays on top of the first ones. 335 | 336 | Args: 337 | delays: A list matching the channels of the Element. If there 338 | are channels=[1, 3], then delays=[1e-3, 0] will delay channel 339 | 1 by 1 ms and channel 3 by nothing. 340 | """ 341 | if len(delays) != len(self.channels): 342 | raise ValueError('Incorrect number of delays specified.' 343 | ' Must match the number of channels.') 344 | 345 | if not sum([d >= 0 for d in delays]) == len(delays): 346 | raise ValueError('Negative delays not allowed.') 347 | 348 | # The strategy is: 349 | # Add waituntil at the beginning, update all waituntils inside, add a 350 | # zeros segment at the end. 351 | # If already-forged arrays are found, simply append and prepend zeros 352 | 353 | SR = self.SR 354 | maxdelay = max(delays) 355 | 356 | for chanind, chan in enumerate(self.channels): 357 | delay = delays[chanind] 358 | 359 | if 'blueprint' in self._data[chan].keys(): 360 | blueprint = self._data[chan]['blueprint'] 361 | 362 | # update existing waituntils 363 | for segpos in range(len(blueprint._funlist)): 364 | if blueprint._funlist[segpos] == 'waituntil': 365 | oldwait = blueprint._argslist[segpos][0] 366 | blueprint._argslist[segpos] = (oldwait+delay,) 367 | # insert delay before the waveform 368 | if delay > 0: 369 | blueprint.insertSegment(0, 'waituntil', (delay,), 370 | 'waituntil') 371 | # add zeros at the end 372 | if maxdelay-delay > 0: 373 | blueprint.insertSegment(-1, PulseAtoms.ramp, (0, 0), 374 | dur=maxdelay-delay) 375 | 376 | else: 377 | arrays = self._data[chan]['array'] 378 | for name, arr in arrays.items(): 379 | pre_wait = np.zeros(int(delay*SR)) 380 | post_wait = np.zeros(int((maxdelay-delay)*SR)) 381 | arrays[name] = np.concatenate((pre_wait, arr, post_wait)) 382 | 383 | def copy(self): 384 | """ 385 | Return a copy of the element 386 | """ 387 | new = Element() 388 | new._data = deepcopy(self._data) 389 | new._meta = deepcopy(self._meta) 390 | return new 391 | 392 | @marked_for_deletion(replaced_by='broadbean.plotting.plotter') 393 | def plotElement(self): 394 | pass 395 | 396 | def __eq__(self, other): 397 | if not isinstance(other, Element): 398 | return False 399 | elif not self._data == other._data: 400 | return False 401 | elif not self._meta == other._meta: 402 | return False 403 | else: 404 | return True 405 | -------------------------------------------------------------------------------- /broadbean/blueprint.py: -------------------------------------------------------------------------------- 1 | # This file is for defining the blueprint object 2 | 3 | import warnings 4 | from inspect import signature 5 | import functools as ft 6 | from typing import List, Dict 7 | 8 | import numpy as np 9 | 10 | from .broadbean import PulseAtoms, marked_for_deletion 11 | 12 | 13 | class SegmentDurationError(Exception): 14 | pass 15 | 16 | 17 | class BluePrint(): 18 | """ 19 | The class of a waveform to become. 20 | """ 21 | 22 | def __init__(self, funlist=None, argslist=None, namelist=None, 23 | marker1=None, marker2=None, segmentmarker1=None, 24 | segmentmarker2=None, SR=None, durslist=None): 25 | """ 26 | Create a BluePrint instance 27 | 28 | Args: 29 | funlist (list): List of functions 30 | argslist (list): List of tuples of arguments 31 | namelist (list): List of names for the functions 32 | marker1 (list): List of marker1 specification tuples 33 | marker2 (list): List of marker2 specifiation tuples 34 | durslist (list): List of durations 35 | 36 | Returns: 37 | BluePrint 38 | """ 39 | # TODO: validate input 40 | 41 | # Sanitising 42 | if funlist is None: 43 | funlist = [] 44 | if argslist is None: 45 | argslist = [] 46 | if namelist is None: 47 | namelist = [] 48 | if durslist is None: 49 | durslist = [] 50 | 51 | # Are the lists of matching lengths? 52 | lenlist = [len(funlist), len(argslist), len(namelist), len(durslist)] 53 | 54 | if len(set(lenlist)) is not 1: 55 | raise ValueError('All input lists must be of same length. ' 56 | 'Received lengths: {}'.format(lenlist)) 57 | # Are the names valid names? 58 | for name in namelist: 59 | if not isinstance(name, str): 60 | raise ValueError('All segment names must be strings. ' 61 | 'Received {}'.format(name)) 62 | elif name is not '': 63 | if name[-1].isdigit(): 64 | raise ValueError('Segment names are not allowed to end' 65 | ' in a number. {} is '.format(name) + 66 | 'therefore not a valid name.') 67 | 68 | self._funlist = funlist 69 | 70 | # Make special functions live in the funlist but transfer their names 71 | # to the namelist 72 | # Infer names from signature if not given, i.e. allow for '' names 73 | for ii, name in enumerate(namelist): 74 | if isinstance(funlist[ii], str): 75 | namelist[ii] = funlist[ii] 76 | elif name == '': 77 | namelist[ii] = funlist[ii].__name__ 78 | 79 | # Allow single arguments to be given as not tuples 80 | for ii, args in enumerate(argslist): 81 | if not isinstance(args, tuple): 82 | argslist[ii] = (args,) 83 | self._argslist = argslist 84 | 85 | self._namelist = namelist 86 | namelist = self._make_names_unique(namelist) 87 | 88 | # initialise markers 89 | if marker1 is None: 90 | self.marker1 = [] 91 | else: 92 | self.marker1 = marker1 93 | if marker2 is None: 94 | self.marker2 = [] 95 | else: 96 | self.marker2 = marker2 97 | if segmentmarker1 is None: 98 | self._segmark1 = [(0, 0)]*len(funlist) 99 | else: 100 | self._segmark1 = segmentmarker1 101 | if segmentmarker2 is None: 102 | self._segmark2 = [(0, 0)]*len(funlist) 103 | else: 104 | self._segmark2 = segmentmarker2 105 | 106 | if durslist is not None: 107 | self._durslist = list(durslist) 108 | else: 109 | self._durslist = None 110 | 111 | self._SR = SR 112 | 113 | @staticmethod 114 | def _basename(string): 115 | """ 116 | Remove trailing numbers from a string. 117 | """ 118 | 119 | if not isinstance(string, str): 120 | raise ValueError('_basename received a non-string input!' 121 | ' Got the following: {}'.format(string)) 122 | 123 | if string == '': 124 | return string 125 | if not(string[-1].isdigit()): 126 | return string 127 | else: 128 | counter = 0 129 | for ss in string[::-1]: 130 | if ss.isdigit(): 131 | counter += 1 132 | else: 133 | break 134 | return string[:-counter] 135 | 136 | # lst = [letter for letter in string if not letter.isdigit()] 137 | # return ''.join(lst) 138 | 139 | @staticmethod 140 | def _make_names_unique(lst): 141 | """ 142 | Make all strings in the input list unique 143 | by appending numbers to reoccuring strings 144 | 145 | Args: 146 | lst (list): List of strings. Intended for the _namelist 147 | 148 | """ 149 | 150 | if not isinstance(lst, list): 151 | raise ValueError('_make_names_unique received a non-list input!' 152 | ' Got {}'.format(lst)) 153 | 154 | baselst = [BluePrint._basename(lstel) for lstel in lst] 155 | uns = np.unique(baselst) 156 | 157 | for un in uns: 158 | inds = [ii for ii, el in enumerate(baselst) if el == un] 159 | for ii, ind in enumerate(inds): 160 | # Do not append numbers to the first occurence 161 | if ii == 0: 162 | lst[ind] = '{}'.format(un) 163 | else: 164 | lst[ind] = '{}{}'.format(un, ii+1) 165 | 166 | return lst 167 | 168 | @property 169 | def length_segments(self): 170 | """ 171 | Returns the number of segments in the blueprint 172 | """ 173 | return len(self._namelist) 174 | 175 | @property 176 | def duration(self): 177 | """ 178 | The total duration of the BluePrint. If necessary, all the arrays 179 | are built. 180 | """ 181 | waits = 'waituntil' in self._funlist 182 | ensavgs = 'ensureaverage_fixed_level' in self._funlist 183 | 184 | if (not(waits) and not(ensavgs)): 185 | return sum(self._durslist) 186 | elif (waits and not(ensavgs)): 187 | waitdurations = self._makeWaitDurations() 188 | return sum(waitdurations) 189 | elif ensavgs: 190 | # TODO: call the forger 191 | raise NotImplementedError('ensureaverage_fixed_level does not' 192 | ' exist yet. Cannot proceed') 193 | 194 | @property 195 | def points(self): 196 | """ 197 | The total number of points in the BluePrint. If necessary, 198 | all the arrays are built. 199 | """ 200 | waits = 'waituntil' in self._funlist 201 | ensavgs = 'ensureaverage_fixed_level' in self._funlist 202 | SR = self.SR 203 | 204 | if SR is None: 205 | raise ValueError('No sample rate specified, can not ' 206 | 'return the number of points.') 207 | 208 | if (not(waits) and not(ensavgs)): 209 | return int(np.round(sum(self._durslist)*SR)) 210 | elif (waits and not(ensavgs)): 211 | waitdurations = self._makeWaitDurations() 212 | return int(np.round(sum(waitdurations)*SR)) 213 | elif ensavgs: 214 | # TODO: call the forger 215 | raise NotImplementedError('ensureaverage_fixed_level does not' 216 | ' exist yet. Cannot proceed') 217 | 218 | @property 219 | def durations(self): 220 | """ 221 | The list of durations 222 | """ 223 | return self._durslist 224 | 225 | @property 226 | def SR(self): 227 | """ 228 | Sample rate of the blueprint 229 | """ 230 | return self._SR 231 | 232 | @property 233 | def description(self): 234 | """ 235 | Returns a dict describing the blueprint. 236 | """ 237 | desc = {} # the dict to return 238 | 239 | no_segs = len(self._namelist) 240 | 241 | for sn in range(no_segs): 242 | segkey = 'segment_{:02d}'.format(sn+1) 243 | desc[segkey] = {} 244 | desc[segkey]['name'] = self._namelist[sn] 245 | if self._funlist[sn] == 'waituntil': 246 | desc[segkey]['function'] = self._funlist[sn] 247 | else: 248 | funname = str(self._funlist[sn])[1:] 249 | funname = funname[:funname.find(' at')] 250 | desc[segkey]['function'] = funname 251 | desc[segkey]['durations'] = self._durslist[sn] 252 | if desc[segkey]['function'] == 'waituntil': 253 | desc[segkey]['arguments'] = {'waittime': self._argslist[sn]} 254 | else: 255 | sig = signature(self._funlist[sn]) 256 | desc[segkey]['arguments'] = dict(zip(sig.parameters, 257 | self._argslist[sn])) 258 | 259 | desc['marker1_abs'] = self.marker1 260 | desc['marker2_abs'] = self.marker2 261 | desc['marker1_rel'] = self._segmark1 262 | desc['marker2_rel'] = self._segmark2 263 | 264 | return desc 265 | 266 | def _makeWaitDurations(self): 267 | """ 268 | Translate waituntills into durations and return that list. 269 | """ 270 | 271 | if 'ensureaverage_fixed_level' in self._funlist: 272 | raise NotImplementedError('There is an "ensureaverage_fixed_level"' 273 | ' in this BluePrint. Cannot compute.') 274 | 275 | funlist = self._funlist.copy() 276 | durations = self._durslist.copy() 277 | argslist = self._argslist 278 | 279 | no_of_waits = funlist.count('waituntil') 280 | 281 | waitpositions = [ii for ii, el in enumerate(funlist) 282 | if el == 'waituntil'] 283 | 284 | # Calculate elapsed times 285 | 286 | for nw in range(no_of_waits): 287 | pos = waitpositions[nw] 288 | funlist[pos] = PulseAtoms.waituntil 289 | elapsed_time = sum(durations[:pos]) 290 | wait_time = argslist[pos][0] 291 | dur = wait_time - elapsed_time 292 | if dur < 0: 293 | raise ValueError('Inconsistent timing. Can not wait until ' + 294 | '{} at position {}.'.format(wait_time, pos) + 295 | ' {} elapsed already'.format(elapsed_time)) 296 | else: 297 | durations[pos] = dur 298 | 299 | return durations 300 | 301 | def showPrint(self): 302 | """ 303 | Pretty-print the contents of the BluePrint. Not finished. 304 | """ 305 | # TODO: tidy up this method and make it use the description property 306 | 307 | if self._durslist is None: 308 | dl = [None]*len(self._namelist) 309 | else: 310 | dl = self._durslist 311 | 312 | datalists = [self._namelist, self._funlist, self._argslist, 313 | dl] 314 | 315 | lzip = zip(*datalists) 316 | 317 | print('Legend: Name, function, arguments, timesteps, durations') 318 | 319 | for ind, (name, fun, args, dur) in enumerate(lzip): 320 | ind_p = ind+1 321 | if fun == 'waituntil': 322 | fun_p = fun 323 | else: 324 | fun_p = fun.__str__().split(' ')[1] 325 | 326 | list_p = [ind_p, name, fun_p, args, dur] 327 | print('Segment {}: "{}", {}, {}, {}'.format(*list_p)) 328 | print('-'*10) 329 | 330 | def changeArg(self, name, arg, value, replaceeverywhere=False): 331 | """ 332 | Change an argument of one or more of the functions in the blueprint. 333 | 334 | Args: 335 | name (str): The name of the segment in which to change an argument 336 | arg (Union[int, str]): Either the position (int) or name (str) of 337 | the argument to change 338 | value (Union[int, float]): The new value of the argument 339 | replaceeverywhere (bool): If True, the same argument is overwritten 340 | in ALL segments where the name matches. E.g. 'gaussian1' will 341 | match 'gaussian', 'gaussian2', etc. If False, only the segment 342 | with exact name match gets a replacement. 343 | 344 | Raises: 345 | ValueError: If the argument can not be matched (either the argument 346 | name does not match or the argument number is wrong). 347 | ValueError: If the name can not be matched. 348 | 349 | """ 350 | # TODO: is there any reason to use tuples internally? 351 | 352 | if replaceeverywhere: 353 | basename = BluePrint._basename 354 | name = basename(name) 355 | nmlst = self._namelist 356 | replacelist = [nm for nm in nmlst if basename(nm) == name] 357 | else: 358 | replacelist = [name] 359 | 360 | # Validation 361 | if name not in self._namelist: 362 | raise ValueError('No segment of that name in blueprint.' 363 | ' Contains segments: {}'.format(self._namelist)) 364 | 365 | for name in replacelist: 366 | 367 | position = self._namelist.index(name) 368 | function = self._funlist[position] 369 | sig = signature(function) 370 | 371 | # Validation 372 | if isinstance(arg, str): 373 | if arg not in sig.parameters: 374 | raise ValueError('No such argument of function ' 375 | '{}.'.format(function.__name__) + 376 | 'Has arguments ' 377 | '{}.'.format(sig.parameters.keys())) 378 | # Each function has two 'secret' arguments, SR and dur 379 | user_params = len(sig.parameters)-2 380 | if isinstance(arg, int) and (arg not in range(user_params)): 381 | raise ValueError('No argument {} '.format(arg) + 382 | 'of function {}.'.format(function.__name__) + 383 | ' Has {} '.format(user_params) + 384 | 'arguments.') 385 | 386 | # allow the user to input single values instead of (val,) 387 | no_of_args = len(self._argslist[position]) 388 | if not isinstance(value, tuple) and no_of_args == 1: 389 | value = (value,) 390 | 391 | if isinstance(arg, str): 392 | for ii, param in enumerate(sig.parameters): 393 | if arg == param: 394 | arg = ii 395 | break 396 | 397 | # Mutating the immutable... 398 | larg = list(self._argslist[position]) 399 | larg[arg] = value 400 | self._argslist[position] = tuple(larg) 401 | 402 | def changeDuration(self, name, dur, replaceeverywhere=False): 403 | """ 404 | Change the duration of one or more segments in the blueprint 405 | 406 | Args: 407 | name (str): The name of the segment in which to change duration 408 | dur (Union[float, int]): The new duration. 409 | replaceeverywhere (Optional[bool]): If True, the duration(s) 410 | is(are) overwritten in ALL segments where the name matches. 411 | E.g. 'gaussian1' will match 'gaussian', 'gaussian2', 412 | etc. If False, only the segment with exact name match 413 | gets a replacement. 414 | 415 | Raises: 416 | ValueError: If durations are not specified for the blueprint 417 | ValueError: If too many or too few durations are given. 418 | ValueError: If no segment matches the name. 419 | ValueError: If dur is not positive 420 | ValueError: If SR is given for the blueprint and dur is less than 421 | 1/SR. 422 | """ 423 | 424 | if (not(isinstance(dur, float)) and not(isinstance(dur, int))): 425 | raise ValueError('New duration must be an int or a float. ' 426 | 'Received {}'.format(type(dur))) 427 | 428 | if replaceeverywhere: 429 | basename = BluePrint._basename 430 | name = basename(name) 431 | nmlst = self._namelist 432 | replacelist = [nm for nm in nmlst if basename(nm) == name] 433 | else: 434 | replacelist = [name] 435 | 436 | # Validation 437 | if name not in self._namelist: 438 | raise ValueError('No segment of that name in blueprint.' 439 | ' Contains segments: {}'.format(self._namelist)) 440 | 441 | for name in replacelist: 442 | position = self._namelist.index(name) 443 | 444 | if dur <= 0: 445 | raise ValueError('Duration must be strictly greater ' 446 | 'than zero.') 447 | 448 | if self.SR is not None: 449 | if dur*self.SR < 1: 450 | raise ValueError('Duration too short! Must be at' 451 | ' least 1/sample rate.') 452 | 453 | self._durslist[position] = dur 454 | 455 | def setSR(self, SR): 456 | """ 457 | Set the associated sample rate 458 | 459 | Args: 460 | SR (Union[int, float]): The sample rate in Sa/s. 461 | """ 462 | self._SR = SR 463 | 464 | def setSegmentMarker(self, name, specs, markerID): 465 | """ 466 | Bind a marker to a specific segment. 467 | 468 | Args: 469 | name (str): Name of the segment 470 | specs (tuple): Marker specification tuple, (delay, duration), 471 | where the delay is relative to the segment start 472 | markerID (int): Which marker channel to output on. Must be 1 or 2. 473 | """ 474 | if markerID not in [1, 2]: 475 | raise ValueError('MarkerID must be either 1 or 2.' 476 | ' Received {}.'.format(markerID)) 477 | 478 | markerselect = {1: self._segmark1, 2: self._segmark2} 479 | position = self._namelist.index(name) 480 | 481 | # TODO: Do we need more than one bound marker per segment? 482 | markerselect[markerID][position] = specs 483 | 484 | def removeSegmentMarker(self, name: str, markerID: int) -> None: 485 | """ 486 | Remove all bound markers from a specific segment 487 | 488 | Args: 489 | name (str): Name of the segment 490 | markerID (int): Which marker channel to remove from (1 or 2). 491 | number (int): The number of the marker, in case several markers are 492 | bound to one element. Default: 1 (the first marker). 493 | """ 494 | if markerID not in [1, 2]: 495 | raise ValueError('MarkerID must be either 1 or 2.' 496 | ' Received {}.'.format(markerID)) 497 | 498 | markerselect = {1: self._segmark1, 2: self._segmark2} 499 | try: 500 | position = self._namelist.index(name) 501 | except ValueError: 502 | raise KeyError('No segment named {} in this BluePrint.' 503 | ''.format(name)) 504 | markerselect[markerID][position] = (0, 0) 505 | 506 | def copy(self): 507 | """ 508 | Returns a copy of the BluePrint 509 | """ 510 | 511 | # Needed because of input validation in __init__ 512 | namelist = [self._basename(name) for name in self._namelist.copy()] 513 | 514 | return BluePrint(self._funlist.copy(), 515 | self._argslist.copy(), 516 | namelist, 517 | self.marker1.copy(), 518 | self.marker2.copy(), 519 | self._segmark1.copy(), 520 | self._segmark2.copy(), 521 | self._SR, 522 | self._durslist) 523 | 524 | def insertSegment(self, pos, func, args=(), dur=None, name=None, 525 | durs=None): 526 | """ 527 | Insert a segment into the bluePrint. 528 | 529 | Args: 530 | pos (int): The position at which to add the segment. Counts like 531 | a python list; 0 is first, -1 is last. Values below -1 are 532 | not allowed, though. 533 | func (function): Function describing the segment. Must have its 534 | duration as the last argument (unless its a special function). 535 | args (Optional[Tuple[Any]]): Tuple of arguments BESIDES duration. 536 | Default: () 537 | dur (Optional[Union[int, float]]): The duration of the 538 | segment. Must be given UNLESS the segment is 539 | 'waituntil' or 'ensureaverage_fixed_level' 540 | name Optional[str]: Name of the segment. If none is given, 541 | the segment will receive the name of its function, 542 | possibly with a number appended. 543 | 544 | Raises: 545 | ValueError: If the position is negative 546 | ValueError: If the name ends in a number 547 | """ 548 | 549 | # Validation 550 | has_ensureavg = ('ensureaverage_fixed_level' in self._funlist or 551 | 'ensureaverage_fixed_dur' in self._funlist) 552 | if func == 'ensureaverage_fixed_level' and has_ensureavg: 553 | raise ValueError('Can not have more than one "ensureaverage"' 554 | ' segment in a blueprint.') 555 | 556 | if durs is not None: 557 | warnings.warn('Deprecation warning: please specify "dur" rather ' 558 | 'than "durs" when inserting a segment') 559 | if dur is None: 560 | dur = durs 561 | else: 562 | raise ValueError('You can not specify "durs" AND "dur"!') 563 | # Take care of 'waituntil' 564 | 565 | # allow users to input single values 566 | if not isinstance(args, tuple): 567 | args = (args,) 568 | 569 | if pos < -1: 570 | raise ValueError('Position must be strictly larger than -1') 571 | 572 | if name is None or name == '': 573 | if func == 'waituntil': 574 | name = 'waituntil' 575 | else: 576 | name = func.__name__ 577 | elif isinstance(name, str): 578 | if len(name) > 0: 579 | if name[-1].isdigit(): 580 | raise ValueError('Segment name must not end in a number') 581 | 582 | if pos == -1: 583 | self._namelist.append(name) 584 | self._namelist = self._make_names_unique(self._namelist) 585 | self._funlist.append(func) 586 | self._argslist.append(args) 587 | self._segmark1.append((0, 0)) 588 | self._segmark2.append((0, 0)) 589 | self._durslist.append(dur) 590 | else: 591 | self._namelist.insert(pos, name) 592 | self._namelist = self._make_names_unique(self._namelist) 593 | self._funlist.insert(pos, func) 594 | self._argslist.insert(pos, args) 595 | self._segmark1.insert(pos, (0, 0)) 596 | self._segmark2.insert(pos, (0, 0)) 597 | self._durslist.insert(pos, dur) 598 | 599 | def removeSegment(self, name): 600 | """ 601 | Remove the specified segment from the blueprint. 602 | 603 | Args: 604 | name (str): The name of the segment to remove. 605 | """ 606 | try: 607 | position = self._namelist.index(name) 608 | except ValueError: 609 | raise KeyError('No segment called {} in blueprint.'.format(name)) 610 | 611 | del self._funlist[position] 612 | del self._argslist[position] 613 | del self._namelist[position] 614 | del self._segmark1[position] 615 | del self._segmark2[position] 616 | del self._durslist[position] 617 | 618 | self._namelist = self._make_names_unique(self._namelist) 619 | 620 | @marked_for_deletion(replaced_by='broadbean.plotting.plotter') 621 | def plot(self, SR=None): 622 | pass 623 | 624 | def __add__(self, other): 625 | """ 626 | Add two BluePrints. The second argument is appended to the first 627 | and a new BluePrint is returned. 628 | 629 | Args: 630 | other (BluePrint): A BluePrint instance 631 | 632 | Returns: 633 | BluePrint: A new blueprint. 634 | 635 | Raises: 636 | ValueError: If the input is not a BluePrint instance 637 | """ 638 | if not isinstance(other, BluePrint): 639 | raise ValueError(""" 640 | BluePrint can only be added to another Blueprint. 641 | Received an object of type {} 642 | """.format(type(other))) 643 | 644 | nl = [self._basename(name) for name in self._namelist] 645 | nl += [self._basename(name) for name in other._namelist] 646 | al = self._argslist + other._argslist 647 | fl = self._funlist + other._funlist 648 | m1 = self.marker1 + other.marker1 649 | m2 = self.marker2 + other.marker2 650 | sm1 = self._segmark1 + other._segmark1 651 | sm2 = self._segmark2 + other._segmark2 652 | dl = self._durslist + other._durslist 653 | 654 | new_bp = BluePrint() 655 | 656 | new_bp._namelist = new_bp._make_names_unique(nl.copy()) 657 | new_bp._funlist = fl.copy() 658 | new_bp._argslist = al.copy() 659 | new_bp.marker1 = m1.copy() 660 | new_bp.marker2 = m2.copy() 661 | new_bp._segmark1 = sm1.copy() 662 | new_bp._segmark2 = sm2.copy() 663 | new_bp._durslist = dl.copy() 664 | 665 | if self.SR is not None: 666 | new_bp.setSR(self.SR) 667 | 668 | return new_bp 669 | 670 | def __eq__(self, other): 671 | """ 672 | Compare two blueprints. They are the same iff all 673 | lists are identical. 674 | 675 | Args: 676 | other (BluePrint): A BluePrint instance 677 | 678 | Returns: 679 | bool: whether the two blueprints are identical 680 | 681 | Raises: 682 | ValueError: If the input is not a BluePrint instance 683 | """ 684 | if not isinstance(other, BluePrint): 685 | raise ValueError(""" 686 | Blueprint can only be compared to another 687 | Blueprint. 688 | Received an object of type {} 689 | """.format(type(other))) 690 | 691 | if not self._namelist == other._namelist: 692 | return False 693 | if not self._funlist == other._funlist: 694 | return False 695 | if not self._argslist == other._argslist: 696 | return False 697 | if not self.marker1 == other.marker1: 698 | return False 699 | if not self.marker2 == other.marker2: 700 | return False 701 | if not self._segmark1 == other._segmark1: 702 | return False 703 | if not self._segmark2 == other._segmark2: 704 | return False 705 | return True 706 | 707 | 708 | def _subelementBuilder(blueprint: BluePrint, SR: int, 709 | durs: List[float]) -> Dict[str, np.ndarray]: 710 | """ 711 | The function building a blueprint, returning a numpy array. 712 | 713 | This is the core translater from description of pulse to actual data points 714 | All arrays must be made with this function 715 | """ 716 | 717 | # Important: building the element must NOT modify any of the mutable 718 | # inputs, therefore all lists are copied 719 | funlist = blueprint._funlist.copy() 720 | argslist = blueprint._argslist.copy() 721 | namelist = blueprint._namelist.copy() 722 | marker1 = blueprint.marker1.copy() 723 | marker2 = blueprint.marker2.copy() 724 | segmark1 = blueprint._segmark1.copy() 725 | segmark2 = blueprint._segmark2.copy() 726 | 727 | durations = durs.copy() 728 | 729 | no_of_waits = funlist.count('waituntil') 730 | 731 | # handle waituntil by translating it into a normal function 732 | waitpositions = [ii for ii, el in enumerate(funlist) if el == 'waituntil'] 733 | 734 | # Calculate elapsed times 735 | 736 | for nw in range(no_of_waits): 737 | pos = waitpositions[nw] 738 | funlist[pos] = PulseAtoms.waituntil 739 | elapsed_time = sum(durations[:pos]) 740 | wait_time = argslist[pos][0] 741 | dur = wait_time - elapsed_time 742 | if dur < 0: 743 | raise ValueError('Inconsistent timing. Can not wait until ' + 744 | '{} at position {}.'.format(wait_time, pos) + 745 | ' {} elapsed already'.format(elapsed_time)) 746 | else: 747 | durations[pos] = dur 748 | 749 | # When special segments like 'waituntil' and 'ensureaverage' get 750 | # evaluated, the list of durations gets updated. That new list 751 | # is newdurations 752 | 753 | newdurations = np.array(durations) 754 | 755 | # All waveforms must ultimately have an integer number of samples 756 | # Now figure out from the durations what these integers are 757 | # 758 | # The most honest thing to do is to simply round off dur*SR 759 | # and raise an exception if the segment ends up with less than 760 | # two points 761 | 762 | intdurations = np.zeros(len(newdurations)) 763 | 764 | for ii, dur in enumerate(newdurations): 765 | int_dur = round(dur*SR) 766 | if int_dur < 2: 767 | raise SegmentDurationError('Too short segment detected! ' 768 | 'Segment "{}" at position {} ' 769 | 'has a duration of {} which at ' 770 | 'an SR of {:.3E} leads to just {} ' 771 | 'point(s). There must be at least ' 772 | '2 points in each segment.' 773 | ''.format(namelist[ii], 774 | ii, 775 | newdurations[ii], 776 | SR, 777 | int_dur)) 778 | else: 779 | intdurations[ii] = int_dur 780 | newdurations[ii] = int_dur/SR 781 | 782 | # The actual forging of the waveform 783 | parts = [ft.partial(fun, *args) for (fun, args) in zip(funlist, argslist)] 784 | blocks = [list(p(SR, d)) for (p, d) in zip(parts, intdurations)] 785 | output = [block for sl in blocks for block in sl] 786 | 787 | # now make the markers 788 | time = np.linspace(0, sum(newdurations), len(output), endpoint=False) 789 | m1 = np.zeros_like(time) 790 | m2 = m1.copy() 791 | 792 | # update the 'absolute time' marker list with 'relative time' 793 | # (segment bound) markers converted to absolute time 794 | elapsed_times = np.cumsum([0.0] + list(newdurations)) 795 | 796 | for pos, spec in enumerate(segmark1): 797 | if spec[1] is not 0: 798 | ontime = elapsed_times[pos] + spec[0] # spec is (delay, duration) 799 | marker1.append((ontime, spec[1])) 800 | for pos, spec in enumerate(segmark2): 801 | if spec[1] is not 0: 802 | ontime = elapsed_times[pos] + spec[0] # spec is (delay, duration) 803 | marker2.append((ontime, spec[1])) 804 | msettings = [marker1, marker2] 805 | marks = [m1, m2] 806 | for marker, setting in zip(marks, msettings): 807 | for (t, dur) in setting: 808 | ind = np.abs(time-t).argmin() 809 | chunk = int(np.round(dur*SR)) 810 | marker[ind:ind+chunk] = 1 811 | 812 | output = np.array(output) # TODO: Why is this sometimes needed? 813 | 814 | outdict = {'wfm': output, 'm1': m1, 'm2': m2, 'time': time, 815 | 'newdurations': newdurations} 816 | 817 | return outdict 818 | -------------------------------------------------------------------------------- /broadbean/sequence.py: -------------------------------------------------------------------------------- 1 | # this file defines the sequence object 2 | # along with a few helpers 3 | import warnings 4 | from copy import deepcopy 5 | from typing import Union, Dict, cast, List, Tuple 6 | import logging 7 | 8 | import numpy as np 9 | from schema import Schema, Or, Optional 10 | 11 | from broadbean.ripasso import applyInverseRCFilter 12 | from broadbean.element import Element # TODO: change import to element.py 13 | from .broadbean import _channelListSorter # TODO: change import to helpers.py 14 | from .broadbean import marked_for_deletion 15 | from .broadbean import PulseAtoms 16 | from .broadbean import _AWGOutput 17 | 18 | log = logging.getLogger(__name__) 19 | 20 | fs_schema = Schema({int: {'type': Or('subsequence', 'element'), 21 | 'content': {int: {'data': {Or(str, int): {str: np.ndarray}}, 22 | Optional('sequencing'): {Optional(str): 23 | int}}}, 24 | 'sequencing': {Optional(str): int}}}) 25 | 26 | 27 | class SequencingError(Exception): 28 | pass 29 | 30 | 31 | class SequenceConsistencyError(Exception): 32 | pass 33 | 34 | 35 | class InvalidForgedSequenceError(Exception): 36 | pass 37 | 38 | 39 | class SequenceCompatibilityError(Exception): 40 | pass 41 | 42 | 43 | class SpecificationInconsistencyError(Exception): 44 | pass 45 | 46 | 47 | class Sequence: 48 | """ 49 | Sequence object 50 | """ 51 | 52 | def __init__(self): 53 | """ 54 | Not much to see here... 55 | """ 56 | 57 | # the internal data structure, a dict with tuples as keys and values 58 | # the key is sequence position (int), the value is element (Element) 59 | # or subsequence (Sequence) 60 | self._data = {} 61 | 62 | # Here goes the sequencing info. Key: position 63 | # value: dict with keys 'twait', 'nrep', 'jump_input', 64 | # 'jump_target', 'goto' 65 | # 66 | # the sequencing is filled out automatically with default values 67 | # when an element is added 68 | # Note that not all output backends use all items in the list 69 | self._sequencing = {} 70 | 71 | # The dictionary to store AWG settings 72 | # Keys will include: 73 | # 'SR', 'channelX_amplitude', 'channelX_offset', 'channelX_filter' 74 | self._awgspecs = {} 75 | 76 | # The metainfo to be extracted by measurements 77 | # todo: I'm pretty sure this is obsolete now that description exists 78 | self._meta = {} 79 | 80 | # some backends (seqx files) allow for a sequence to have a name 81 | # we make the name a property of the sequence 82 | self._name = '' 83 | 84 | def __eq__(self, other): 85 | if not isinstance(other, Sequence): 86 | return False 87 | elif not self._data == other._data: 88 | return False 89 | elif not self._meta == other._meta: 90 | return False 91 | elif not self._awgspecs == other._awgspecs: 92 | return False 93 | elif not self._sequencing == other._sequencing: 94 | return False 95 | else: 96 | return True 97 | 98 | def __add__(self, other): 99 | """ 100 | Add two sequences. 101 | Return a new sequence with is the right argument appended to the 102 | left argument. 103 | """ 104 | 105 | # Validation 106 | if not self.checkConsistency(): 107 | raise SequenceConsistencyError('Left hand sequence inconsistent!') 108 | if not other.checkConsistency(): 109 | raise SequenceConsistencyError('Right hand sequence inconsistent!') 110 | 111 | if not self._awgspecs == other._awgspecs: 112 | raise SequenceCompatibilityError('Incompatible sequences: ' 113 | 'different AWG' 114 | 'specifications.') 115 | 116 | newseq = Sequence() 117 | N = len(self._data) 118 | 119 | newdata1 = dict([(key, self.element(key).copy()) 120 | for key in self._data.keys()]) 121 | newdata2 = dict([(key+N, other.element(key).copy()) 122 | for key in other._data.keys()]) 123 | newdata1.update(newdata2) 124 | 125 | newseq._data = newdata1 126 | 127 | newsequencing1 = dict([(key, self._sequencing[key].copy()) 128 | for key in self._sequencing.keys()]) 129 | newsequencing2 = dict() 130 | 131 | for key, item in other._sequencing.items(): 132 | newitem = item.copy() 133 | # update goto and jump according to new sequence length 134 | if newitem['goto'] > 0: 135 | newitem['goto'] += N 136 | if newitem['jump_target'] > 0: 137 | newitem['jump_target'] += N 138 | newsequencing2.update({key+N: newitem}) 139 | 140 | newsequencing1.update(newsequencing2) 141 | 142 | newseq._sequencing = newsequencing1 143 | 144 | newseq._awgspecs = other._awgspecs.copy() 145 | 146 | return newseq 147 | 148 | def copy(self): 149 | """ 150 | Returns a copy of the sequence. 151 | """ 152 | newseq = Sequence() 153 | newseq._data = deepcopy(self._data) 154 | newseq._meta = deepcopy(self._meta) 155 | newseq._awgspecs = deepcopy(self._awgspecs) 156 | newseq._sequencing = deepcopy(self._sequencing) 157 | 158 | return newseq 159 | 160 | def setSequenceSettings(self, pos, wait, nreps, jump, goto): 161 | """ 162 | Set the sequence setting for the sequence element at pos. 163 | 164 | Args: 165 | pos (int): The sequence element (counting from 1) 166 | wait (int): The wait state specifying whether to wait for a 167 | trigger. 0: OFF, don't wait, 1: ON, wait. For some backends, 168 | additional integers are allowed to specify the trigger input. 169 | 0 always means off. 170 | nreps (int): Number of repetitions. 0 corresponds to infinite 171 | repetitions 172 | jump (int): Event jump target, the position of a sequence element. 173 | If 0, the event jump state is off. 174 | goto (int): Goto target, the position of a sequence element. 175 | 0 means next. 176 | """ 177 | 178 | warnings.warn('Deprecation warning. This function is only compatible ' 179 | 'with AWG5014 output and will be removed. ' 180 | 'Please use the specific setSequencingXXX methods.') 181 | 182 | # Validation (some validation 'postponed' and put in checkConsistency) 183 | # 184 | # Because of different compliances for different backends, 185 | # most validation of these settings is deferred and performed 186 | # in the outputForXXX methods 187 | 188 | self._sequencing[pos] = {'twait': wait, 'nrep': nreps, 189 | 'jump_target': jump, 'goto': goto, 190 | 'jump_input': 0} 191 | 192 | def setSequencingTriggerWait(self, pos: int, wait: int) -> None: 193 | """ 194 | Set the trigger wait for the sequence element at pos. For 195 | AWG 5014 out, this can be 0 or 1, For AWG 70000A output, this 196 | can be 0, 1, 2, or 3. 197 | 198 | Args: 199 | pos: The sequence element (counting from 1) 200 | wait: The wait state/input depending on backend. 201 | """ 202 | self._sequencing[pos]['twait'] = wait 203 | 204 | def setSequencingNumberOfRepetitions(self, pos: int, nrep: int) -> None: 205 | """ 206 | Set the number of repetitions for the sequence element at pos. 207 | 208 | Args: 209 | pos: The sequence element (counting from 1) 210 | nrep: The number of repetitions (0 means infinite) 211 | """ 212 | self._sequencing[pos]['nrep'] = nrep 213 | 214 | def setSequencingEventInput(self, pos: int, jump_input: int) -> None: 215 | """ 216 | Set the event input for the sequence element at pos. This setting is 217 | ignored by the AWG 5014. 218 | 219 | Args: 220 | pos: The sequence element (counting from 1) 221 | jump_input: The input specifier, 0 for off, 222 | 1 for 'TrigA', 2 for 'TrigB', 3 for 'Internal'. 223 | """ 224 | self._sequencing[pos]['jump_input'] = jump_input 225 | 226 | def setSequencingEventJumpTarget(self, pos: int, jump_target: int) -> None: 227 | """ 228 | Set the event jump target for the sequence element at pos. 229 | 230 | Args: 231 | pos: The sequence element (counting from 1) 232 | jump_target: The sequence element to jump to (counting from 1) 233 | """ 234 | self._sequencing[pos]['jump_target'] = jump_target 235 | 236 | def setSequencingGoto(self, pos: int, goto: int) -> None: 237 | """ 238 | Set the goto target (which element to play after the current one ends) 239 | for the sequence element at pos. 240 | 241 | Args: 242 | pos: The sequence element (counting from 1) 243 | goto: The position of the element to play. 0 means 'next in line' 244 | """ 245 | self._sequencing[pos]['goto'] = goto 246 | 247 | def setSR(self, SR): 248 | """ 249 | Set the sample rate for the sequence 250 | """ 251 | self._awgspecs['SR'] = SR 252 | 253 | def setChannelVoltageRange(self, channel, ampl, offset): 254 | """ 255 | Assign the physical voltages of the channel. This is used when making 256 | output for .awg files. The corresponding parameters in the QCoDeS 257 | AWG5014 driver are called chXX_amp and chXX_offset. Please ensure that 258 | the channel in question is indeed in ampl/offset mode and not in 259 | high/low mode. 260 | 261 | Args: 262 | channel (int): The channel number 263 | ampl (float): The channel peak-to-peak amplitude (V) 264 | offset (float): The channel offset (V) 265 | """ 266 | warnings.warn('Deprecation warning. This function is deprecated.' 267 | ' Use setChannelAmplitude and SetChannelOffset ' 268 | 'instead.') 269 | 270 | keystr = 'channel{}_amplitude'.format(channel) 271 | self._awgspecs[keystr] = ampl 272 | keystr = 'channel{}_offset'.format(channel) 273 | self._awgspecs[keystr] = offset 274 | 275 | def setChannelAmplitude(self, channel: Union[int, str], 276 | ampl: float) -> None: 277 | """ 278 | Assign the physical voltage amplitude of the channel. This is used 279 | when making output for real instruments. 280 | 281 | Args: 282 | channel: The channel number 283 | ampl: The channel peak-to-peak amplitude (V) 284 | """ 285 | keystr = 'channel{}_amplitude'.format(channel) 286 | self._awgspecs[keystr] = ampl 287 | 288 | def setChannelOffset(self, channel: Union[int, str], 289 | offset: float) -> None: 290 | """ 291 | Assign the physical voltage offset of the channel. This is used 292 | by some backends when making output for real instruments 293 | 294 | Args: 295 | channel: The channel number/name 296 | offset: The channel offset (V) 297 | """ 298 | keystr = 'channel{}_offset'.format(channel) 299 | self._awgspecs[keystr] = offset 300 | 301 | def setChannelDelay(self, channel: Union[int, str], 302 | delay: float) -> None: 303 | """ 304 | Assign a delay to a channel. This is used when making output for .awg 305 | files. Use the delay to compensate for cable length differences etc. 306 | Zeros are prepended to the waveforms to delay them and correspondingly 307 | appended to non (or less) delayed channels. 308 | 309 | Args: 310 | channel: The channel number/name 311 | delay: The required delay (s) 312 | 313 | Raises: 314 | ValueError: If a non-integer or non-non-negative channel number is 315 | given. 316 | """ 317 | 318 | self._awgspecs['channel{}_delay'.format(channel)] = delay 319 | 320 | def setChannelFilterCompensation(self, channel: Union[str, int], 321 | kind: str, order: int=1, 322 | f_cut: float=None, 323 | tau: float=None) -> None: 324 | """ 325 | Specify a filter to compensate for. 326 | 327 | The specified channel will get a compensation (pre-distorion) to 328 | compensate for the specified frequency filter. Just to be clear: 329 | the INVERSE transfer function of the one you specify is applied. 330 | Only compensation for simple RC-circuit type high pass and low 331 | pass is supported. 332 | 333 | Args: 334 | channel: The channel to apply this to. 335 | kind: Either 'LP' or 'HP' 336 | order: The order of the filter to compensate for. 337 | May be negative. Default: 1. 338 | f_cut: The cut_off frequency (Hz). 339 | tau): The time constant (s). Note that 340 | tau = 1/f_cut and that only one of the two can be specified. 341 | 342 | Raises: 343 | ValueError: If kind is not 'LP' or 'HP' 344 | ValueError: If order is not an int. 345 | SpecificationInconsistencyError: If both f_cut and tau are given. 346 | """ 347 | 348 | if kind not in ['HP', 'LP']: 349 | raise ValueError('Filter kind must either be "LP" (low pass) or ' 350 | '"HP" (high pass).') 351 | if not isinstance(order, int): 352 | raise ValueError('Filter order must be an integer.') 353 | if (f_cut is not None) and (tau is not None): 354 | raise SpecificationInconsistencyError('Can not specify BOTH a time' 355 | ' constant and a cut-off ' 356 | 'frequency.') 357 | 358 | keystr = 'channel{}_filtercompensation'.format(channel) 359 | self._awgspecs[keystr] = {'kind': kind, 'order': order, 'f_cut': f_cut, 360 | 'tau': tau} 361 | 362 | def addElement(self, position: int, element: Element) -> None: 363 | """ 364 | Add an element to the sequence. Overwrites previous values. 365 | 366 | Args: 367 | position (int): The sequence position of the element (lowest: 1) 368 | element (Element): An element instance 369 | 370 | Raises: 371 | ValueError: If the element has inconsistent durations 372 | """ 373 | 374 | # Validation 375 | element.validateDurations() 376 | 377 | # make a new copy of the element 378 | newelement = element.copy() 379 | 380 | # Data mutation 381 | self._data.update({position: newelement}) 382 | 383 | # insert default sequencing settings 384 | self._sequencing[position] = {'twait': 0, 'nrep': 1, 385 | 'jump_input': 0, 'jump_target': 0, 386 | 'goto': 0} 387 | 388 | def addSubSequence(self, position: int, subsequence: 'Sequence') -> None: 389 | """ 390 | Add a subsequence to the sequence. Overwrites anything previously 391 | assigned to this position. The subsequence can not contain any 392 | subsequences itself. 393 | 394 | Args: 395 | position: The sequence position (starting from 1) 396 | subsequence: The subsequence to add 397 | """ 398 | if not isinstance(subsequence, Sequence): 399 | raise ValueError('Subsequence must be a sequence object. ' 400 | 'Received object of type ' 401 | '{}.'.format(type(subsequence))) 402 | 403 | for elem in subsequence._data.values(): 404 | if isinstance(elem, Sequence): 405 | raise ValueError('Subsequences can not contain subsequences.') 406 | 407 | if subsequence.SR != self.SR: 408 | raise ValueError('Subsequence SR does not match (main) sequence SR' 409 | '. ({} and {}).'.format(subsequence.SR, self.SR)) 410 | 411 | self._data[position] = subsequence.copy() 412 | 413 | self._sequencing[position] = {'twait': 0, 'nrep': 1, 414 | 'jump_input': 0, 'jump_target': 0, 415 | 'goto': 0} 416 | 417 | def checkConsistency(self, verbose=False): 418 | """ 419 | Checks wether the sequence can be built, i.e. wether all elements 420 | have waveforms on the same channels and of the same length. 421 | """ 422 | # TODO: Give helpful info if the check fails 423 | 424 | try: 425 | self._awgspecs['SR'] 426 | except KeyError: 427 | raise KeyError('No sample rate specified. Can not perform check') 428 | 429 | # First check that all sample rates agree 430 | # Since all elements are validated on input, the SR exists 431 | SRs = [elem.SR for elem in self._data.values()] 432 | if SRs == []: # case of empty Sequence 433 | SRs = [None] 434 | if SRs.count(SRs[0]) != len(SRs): 435 | failmssg = ('checkConsistency failed: inconsistent sample rates.') 436 | log.info(failmssg) 437 | if verbose: 438 | print(failmssg) 439 | return False 440 | 441 | # Then check that elements use the same channels 442 | specchans = [] 443 | for elem in self._data.values(): 444 | chans = _channelListSorter(elem.channels) 445 | specchans.append(chans) 446 | if specchans == []: # case of empty Sequence 447 | chans = None 448 | specchans = [None] 449 | if specchans.count(chans) != len(specchans): 450 | failmssg = ('checkConsistency failed: different elements specify ' 451 | 'different channels') 452 | log.info(failmssg) 453 | if verbose: 454 | print(failmssg) 455 | return False 456 | 457 | # TODO: must all elements have same length? Does any AWG require this? 458 | 459 | # Finally, check that all positions are filled 460 | positions = list(self._data.keys()) 461 | if positions == []: # case of empty Sequence 462 | positions = [1] 463 | if not positions == list(range(1, len(positions)+1)): 464 | failmssg = ('checkConsistency failed: inconsistent sequence' 465 | 'positions. Must be 1, 2, 3, ...') 466 | log.info(failmssg) 467 | if verbose: 468 | print(failmssg) 469 | return False 470 | 471 | # If all three tests pass... 472 | return True 473 | 474 | @property 475 | def description(self): 476 | """ 477 | Return a dictionary fully describing the Sequence. 478 | """ 479 | desc = {} 480 | 481 | for pos, elem in self._data.items(): 482 | desc[str(pos)] = {} 483 | desc[str(pos)]['channels'] = elem.description 484 | try: 485 | sequencing = self._sequencing[pos] 486 | seqdict = {'Wait trigger': sequencing[0], 487 | 'Repeat': sequencing[1], 488 | 'Event jump to': sequencing[2], 489 | 'Go to': sequencing[3]} 490 | desc[str(pos)]['sequencing'] = seqdict 491 | except KeyError: 492 | desc[str(pos)]['sequencing'] = 'Not set' 493 | 494 | return desc 495 | 496 | @property 497 | def name(self): 498 | return self._name 499 | 500 | @name.setter 501 | def name(self, newname): 502 | if not isinstance(newname, str): 503 | raise ValueError('The sequence name must be a string') 504 | self._name = newname 505 | 506 | @property 507 | def length_sequenceelements(self): 508 | """ 509 | Returns the current number of specified sequence elements 510 | """ 511 | return len(self._data) 512 | 513 | @property 514 | def SR(self): 515 | """ 516 | Returns the sample rate, if defined. Else returns -1. 517 | """ 518 | try: 519 | SR = self._awgspecs['SR'] 520 | except KeyError: 521 | SR = -1 522 | 523 | return SR 524 | 525 | @property 526 | def channels(self): 527 | """ 528 | Returns a list of the specified channels of the sequence 529 | """ 530 | if self.checkConsistency(): 531 | return self.element(1).channels 532 | else: 533 | raise SequenceConsistencyError('Sequence not consistent. Can not' 534 | ' figure out the channels.') 535 | 536 | @property 537 | def points(self): 538 | """ 539 | Returns the number of points of the sequence, disregarding 540 | sequencing info (like repetitions). Useful for asserting upload 541 | times, i.e. the size of the built sequence. 542 | """ 543 | total = 0 544 | for elem in self._data.values(): 545 | total += elem.points 546 | return total 547 | 548 | def element(self, pos): 549 | """ 550 | Returns the element at the given position. Changes made to the return 551 | value of this methods will apply to the sequence. If this is undesired, 552 | make a copy of the returned element using Element.copy 553 | 554 | Args: 555 | pos (int): The sequence position 556 | 557 | Raises: 558 | KeyError: If no element is specified at the given position 559 | """ 560 | try: 561 | elem = self._data[pos] 562 | except KeyError: 563 | raise KeyError('No element specified at sequence ' 564 | 'position {}'.format(pos)) 565 | 566 | return elem 567 | 568 | @staticmethod 569 | def _plotSummary(seq: Dict[int, Dict]) -> Dict[int, Dict[str, np.ndarray]]: 570 | """ 571 | Return a plotting summary of a subsequence. 572 | 573 | Args: 574 | seq: The 'content' value of a forged sequence where a 575 | subsequence resides 576 | 577 | Returns: 578 | A dict that looks like a forged element, but all waveforms 579 | are just two points, np.array([min, max]) 580 | """ 581 | 582 | output = {} 583 | 584 | # we assume correctness, all postions specify the same channels 585 | chans = seq[1]['data'].keys() 586 | 587 | minmax = dict(zip(chans, [(0, 0)]*len(chans))) 588 | 589 | for element in seq.values(): 590 | 591 | arr_dict = element['data'] 592 | 593 | for chan in chans: 594 | wfm = arr_dict[chan]['wfm'] 595 | if wfm.min() < minmax[chan][0]: 596 | minmax[chan] = (wfm.min(), minmax[chan][1]) 597 | if wfm.max() > minmax[chan][1]: 598 | minmax[chan] = (minmax[chan][0], wfm.max()) 599 | output[chan] = {'wfm': np.array(minmax[chan]), 600 | 'm1': np.zeros(2), 601 | 'm2': np.zeros(2), 602 | 'time': np.linspace(0, 1, 2)} 603 | 604 | return output 605 | 606 | @marked_for_deletion(replaced_by='broadbean.plotting.plotter') 607 | def plotSequence(self) -> None: 608 | pass 609 | 610 | @marked_for_deletion(replaced_by='broadbean.plotting.plotter') 611 | def plotAWGOutput(self): 612 | pass 613 | 614 | def forge(self, apply_delays: bool=True, 615 | apply_filters: bool=True, 616 | includetime: bool=False) -> Dict[int, Dict]: 617 | """ 618 | Forge the sequence, applying all specified transformations 619 | (delays and ripasso filter corrections). Copies the data, so 620 | that the sequence is not modified by forging. 621 | 622 | Args: 623 | apply_delays: Whether to apply the assigned channel delays 624 | (if any) 625 | apply_filters: Whether to apply the assigned channel filters 626 | (if any) 627 | includetime: Whether to include the time axis and the segment 628 | durations (a list) with the arrays. Used for plotting. 629 | 630 | Returns: 631 | A nested dictionary holding the forged sequence. 632 | """ 633 | # Validation 634 | if not self.checkConsistency(): 635 | raise ValueError('Can not generate output. Something is ' 636 | 'inconsistent. Please run ' 637 | 'checkConsistency(verbose=True) for more details') 638 | 639 | output: Dict[int, Dict] = {} 640 | channels = self.channels 641 | data = deepcopy(self._data) 642 | seqlen = len(data.keys()) 643 | 644 | # TODO: in this function, we iterate through the sequence three times 645 | # It is probably worth considering refactoring that into a single 646 | # iteration, although that may compromise readability 647 | 648 | # Apply channel delays. 649 | 650 | if apply_delays: 651 | delays = [] 652 | for chan in channels: 653 | try: 654 | delays.append(self._awgspecs[f'channel{chan}_delay']) 655 | except KeyError: 656 | delays.append(0) 657 | 658 | for pos in range(1, seqlen+1): 659 | if isinstance(data[pos], Sequence): 660 | subseq = data[pos] 661 | for elem in subseq._data.values(): 662 | elem._applyDelays(delays) 663 | elif isinstance(data[pos], Element): 664 | data[pos]._applyDelays(delays) 665 | 666 | # forge arrays and form the output dict 667 | for pos in range(1, seqlen+1): 668 | output[pos] = {} 669 | output[pos]['sequencing'] = self._sequencing[pos] 670 | if isinstance(data[pos], Sequence): 671 | subseq = data[pos] 672 | output[pos]['type'] = 'subsequence' 673 | output[pos]['content'] = {} 674 | for pos2 in range(1, subseq.length_sequenceelements+1): 675 | output[pos]['content'][pos2] = {'data': {}, 676 | 'sequencing': {}} 677 | elem = subseq.element(pos2) 678 | dictdata = elem.getArrays(includetime=includetime) 679 | output[pos]['content'][pos2]['data'] = dictdata 680 | seqing = subseq._sequencing[pos2] 681 | output[pos]['content'][pos2]['sequencing'] = seqing 682 | # TODO: update sequencing 683 | elif isinstance(data[pos], Element): 684 | elem = data[pos] 685 | output[pos]['type'] = 'element' 686 | dictdata = elem.getArrays(includetime=includetime) 687 | output[pos]['content'] = {1: {'data': dictdata}} 688 | 689 | # apply filter corrections to forged arrays 690 | if apply_filters: 691 | for pos1 in range(1, seqlen+1): 692 | thiselem = output[pos1]['content'] 693 | for pos2 in thiselem.keys(): 694 | data = thiselem[pos2]['data'] 695 | for channame in data.keys(): 696 | keystr = f'channel{channame}_filtercompensation' 697 | if keystr in self._awgspecs.keys(): 698 | kind = self._awgspecs[keystr]['kind'] 699 | order = self._awgspecs[keystr]['order'] 700 | f_cut = self._awgspecs[keystr]['f_cut'] 701 | tau = self._awgspecs[keystr]['tau'] 702 | if f_cut is None: 703 | f_cut = 1/tau 704 | prefilter = data[channame]['wfm'] 705 | postfilter = applyInverseRCFilter(prefilter, 706 | self.SR, 707 | kind, 708 | f_cut, order, 709 | DCgain=1) 710 | (output[pos1] 711 | ['content'] 712 | [pos2] 713 | ['data'] 714 | [channame] 715 | ['wfm']) = postfilter 716 | 717 | return output 718 | 719 | def _prepareForOutputting(self) -> List[Dict[int, np.ndarray]]: 720 | """ 721 | The preparser for numerical output. Applies delay and ripasso 722 | corrections. 723 | 724 | Returns: 725 | A list of outputs of the Element's getArrays functions, i.e. 726 | a list of dictionaries with key position (int) and value 727 | an np.ndarray of array([wfm, m1, m2, time]), where the 728 | wfm values are still in V. The particular backend output 729 | function must rescale to the specific format it adheres to. 730 | """ 731 | # Validation 732 | if not self.checkConsistency(): 733 | raise ValueError('Can not generate output. Something is ' 734 | 'inconsistent. Please run ' 735 | 'checkConsistency(verbose=True) for more details') 736 | # 737 | # 738 | channels = self.element(1).channels # all elements have ident. chans 739 | # We copy the data so that the state of the Sequence is left unaltered 740 | # by outputting for AWG 741 | data = deepcopy(self._data) 742 | seqlen = len(data.keys()) 743 | # check if sequencing information is specified for each element 744 | if not sorted(list(self._sequencing.keys())) == list(range(1, seqlen+1)): 745 | raise ValueError('Can not generate output for file; ' 746 | 'incorrect sequencer information.') 747 | 748 | # Verify physical amplitude specifiations 749 | for chan in channels: 750 | ampkey = 'channel{}_amplitude'.format(chan) 751 | if ampkey not in self._awgspecs.keys(): 752 | raise KeyError('No amplitude specified for channel ' 753 | '{}. Can not continue.'.format(chan)) 754 | 755 | # Apply channel delays. 756 | delays = [] 757 | for chan in channels: 758 | try: 759 | delays.append(self._awgspecs['channel{}_delay'.format(chan)]) 760 | except KeyError: 761 | delays.append(0) 762 | maxdelay = max(delays) 763 | 764 | for pos in range(1, seqlen+1): 765 | for chanind, chan in enumerate(channels): 766 | element = data[pos] 767 | delay = delays[chanind] 768 | 769 | if 'blueprint' in element._data[chan].keys(): 770 | blueprint = element._data[chan]['blueprint'] 771 | 772 | # update existing waituntils 773 | for segpos in range(len(blueprint._funlist)): 774 | if blueprint._funlist[segpos] == 'waituntil': 775 | oldwait = blueprint._argslist[segpos][0] 776 | blueprint._argslist[segpos] = (oldwait+delay,) 777 | # insert delay before the waveform 778 | if delay > 0: 779 | blueprint.insertSegment(0, 'waituntil', (delay,), 780 | 'waituntil') 781 | # add zeros at the end 782 | if maxdelay-delay > 0: 783 | blueprint.insertSegment(-1, PulseAtoms.ramp, (0, 0), 784 | dur=maxdelay-delay) 785 | # TODO: is the next line even needed? 786 | element.addBluePrint(chan, blueprint) 787 | 788 | else: 789 | arrays = element[chan]['array'] 790 | for name, arr in arrays.items(): 791 | pre_wait = np.zeros(int(delay/self.SR)) 792 | post_wait = np.zeros(int((maxdelay-delay)/self.SR)) 793 | arrays[name] = np.concatenate((pre_wait, arr, 794 | post_wait)) 795 | 796 | # Now forge all the elements as specified 797 | elements = [] # the forged elements 798 | for pos in range(1, seqlen+1): 799 | elements.append(data[pos].getArrays()) 800 | 801 | # Now that the numerical arrays exist, we can apply filter compensation 802 | for chan in channels: 803 | keystr = 'channel{}_filtercompensation'.format(chan) 804 | if keystr in self._awgspecs.keys(): 805 | kind = self._awgspecs[keystr]['kind'] 806 | order = self._awgspecs[keystr]['order'] 807 | f_cut = self._awgspecs[keystr]['f_cut'] 808 | tau = self._awgspecs[keystr]['tau'] 809 | if f_cut is None: 810 | f_cut = 1/tau 811 | for pos in range(seqlen): 812 | prefilter = elements[pos][chan]['wfm'] 813 | postfilter = applyInverseRCFilter(prefilter, 814 | self.SR, 815 | kind, f_cut, order, 816 | DCgain=1) 817 | elements[pos][chan]['wfm'] = postfilter 818 | 819 | return elements 820 | 821 | def outputForSEQXFile(self) -> Tuple[List[int], List[int], List[int], 822 | List[int], List[int], 823 | List[List[np.ndarray]], 824 | List[float], str]: 825 | """ 826 | Generate a tuple matching the call signature of the QCoDeS 827 | AWG70000A driver's `makeSEQXFile` function. If channel delays 828 | have been specified, they are added to the ouput before exporting. 829 | The intended use of this function together with the QCoDeS driver is 830 | 831 | .. code:: python 832 | 833 | pkg = seq.outputForSEQXFile() 834 | seqx = awg70000A.makeSEQXFile(*pkg) 835 | 836 | Returns: 837 | A tuple holding (trig_waits, nreps, event_jumps, event_jump_to, 838 | go_to, wfms, amplitudes, seqname) 839 | """ 840 | 841 | # most of the footwork is done by the following function 842 | elements = self._prepareForOutputting() 843 | # _prepareForOutputting asserts that channel amplitudes and 844 | # full sequencing is specified 845 | seqlen = len(elements) 846 | # all elements have ident. chans since _prepareForOutputting 847 | # did not raise an exception 848 | channels = self.element(1).channels 849 | 850 | for chan in channels: 851 | offkey = 'channel{}_offset'.format(chan) 852 | if offkey in self._awgspecs.keys(): 853 | log.warning("Found a specified offset for channel " 854 | "{}, but .seqx files can't contain offset " 855 | "information. Will ignore the offset." 856 | "".format(chan)) 857 | 858 | # now check that the amplitudes are within the allowed limits 859 | # also verify that all waveforms are at least 2400 points 860 | # No rescaling because the driver's _makeWFMXBinaryData does 861 | # the rescaling 862 | 863 | amplitudes = [] 864 | for chan in channels: 865 | ampl = self._awgspecs['channel{}_amplitude'.format(chan)] 866 | amplitudes.append(ampl) 867 | if len(amplitudes) == 1: 868 | amplitudes.append(0) 869 | 870 | for pos in range(1, seqlen+1): 871 | element = elements[pos-1] 872 | for chan in channels: 873 | ampl = self._awgspecs['channel{}_amplitude'.format(chan)] 874 | wfm = element[chan]['wfm'] 875 | # check the waveform length 876 | if len(wfm) < 2400: 877 | raise ValueError('Waveform too short on channel ' 878 | '{} at step {}; only {} points. ' 879 | 'The required minimum is 2400 points.' 880 | ''.format(chan, pos, len(wfm))) 881 | # check whether the waveform voltages can be realised 882 | if wfm.max() > ampl/2: 883 | raise ValueError('Waveform voltages exceed channel range ' 884 | 'on channel {}'.format(chan) + 885 | ' sequence element {}.'.format(pos) + 886 | ' {} > {}!'.format(wfm.max(), ampl/2)) 887 | if wfm.min() < -ampl/2: 888 | raise ValueError('Waveform voltages exceed channel range ' 889 | 'on channel {}'.format(chan) + 890 | ' sequence element {}. '.format(pos) + 891 | '{} < {}!'.format(wfm.min(), -ampl/2)) 892 | element[chan]['wfm'] = wfm 893 | elements[pos-1] = element 894 | 895 | # Finally cast the lists into the shapes required by the AWG driver 896 | 897 | waveforms = cast(List[List[np.ndarray]], 898 | [[] for dummy in range(len(channels))]) 899 | nreps = [] 900 | trig_waits = [] 901 | gotos = [] 902 | jump_states = [] 903 | jump_tos = [] 904 | 905 | # Since sequencing options are valid/invalid differently for 906 | # different backends, we make the validation here 907 | for pos in range(1, seqlen+1): 908 | for chanind, chan in enumerate(channels): 909 | wfm = elements[pos-1][chan]['wfm'] 910 | m1 = elements[pos-1][chan]['m1'] 911 | m2 = elements[pos-1][chan]['m2'] 912 | waveforms[chanind].append(np.array([wfm, m1, m2])) 913 | 914 | twait = self._sequencing[pos]['twait'] 915 | nrep = self._sequencing[pos]['nrep'] 916 | jump_to = self._sequencing[pos]['jump_target'] 917 | jump_state = self._sequencing[pos]['jump_input'] 918 | goto = self._sequencing[pos]['goto'] 919 | 920 | if twait not in [0, 1, 2, 3]: 921 | raise SequencingError('Invalid trigger input at position' 922 | '{}: {}. Must be 0, 1, 2, or 3.' 923 | ''.format(pos, twait)) 924 | 925 | if jump_state not in [0, 1, 2, 3]: 926 | raise SequencingError('Invalid event jump input at position' 927 | '{}: {}. Must be either 0, 1, 2, or 3.' 928 | ''.format(pos, twait)) 929 | 930 | if nrep not in range(0, 16384): 931 | raise SequencingError('Invalid number of repetions at position' 932 | '{}: {}. Must be either 0 (infinite) ' 933 | 'or 1-16,383.'.format(pos, nrep)) 934 | 935 | if jump_to not in range(-1, seqlen+1): 936 | raise SequencingError('Invalid event jump target at position' 937 | '{}: {}. Must be either -1 (next),' 938 | ' 0 (off), or 1-{}.' 939 | ''.format(pos, jump_to, seqlen)) 940 | 941 | if goto not in range(0, seqlen+1): 942 | raise SequencingError('Invalid goto target at position' 943 | '{}: {}. Must be either 0 (next),' 944 | ' or 1-{}.' 945 | ''.format(pos, goto, seqlen)) 946 | 947 | trig_waits.append(twait) 948 | nreps.append(nrep) 949 | jump_tos.append(jump_to) 950 | jump_states.append(jump_state) 951 | gotos.append(goto) 952 | 953 | return (trig_waits, nreps, jump_states, jump_tos, gotos, 954 | waveforms, amplitudes, self.name) 955 | 956 | def outputForAWGFile(self): 957 | """ 958 | Returns a sliceable object with items matching the call 959 | signature of the 'make_*_awg_file' functions of the QCoDeS 960 | AWG5014 driver. One may then construct an awg file as follows 961 | (assuming that seq is the sequence object): 962 | 963 | .. code:: python 964 | 965 | package = seq.outputForAWGFile() 966 | make_awg_file(*package[:], **kwargs) 967 | 968 | 969 | """ 970 | 971 | elements = self._prepareForOutputting() 972 | seqlen = len(elements) 973 | # all elements have ident. chans since _prepareForOutputting 974 | # did not raise an exception 975 | channels = self.element(1).channels 976 | 977 | for chan in channels: 978 | offkey = 'channel{}_offset'.format(chan) 979 | if offkey not in self._awgspecs.keys(): 980 | raise ValueError("No specified offset for channel " 981 | "{}, can not continue." 982 | "".format(chan)) 983 | 984 | # Apply channel scaling 985 | # We must rescale to the interval -1, 1 where 1 is ampl/2+off and -1 is 986 | # -ampl/2+off. 987 | # 988 | def rescaler(val, ampl, off): 989 | return val/ampl*2-off 990 | for pos in range(1, seqlen+1): 991 | element = elements[pos-1] 992 | for chan in channels: 993 | ampl = self._awgspecs['channel{}_amplitude'.format(chan)] 994 | off = self._awgspecs['channel{}_offset'.format(chan)] 995 | wfm = element[chan]['wfm'] 996 | # check whether the waveform voltages can be realised 997 | if wfm.max() > ampl/2+off: 998 | raise ValueError('Waveform voltages exceed channel range ' 999 | 'on channel {}'.format(chan) + 1000 | ' sequence element {}.'.format(pos) + 1001 | ' {} > {}!'.format(wfm.max(), ampl/2+off)) 1002 | if wfm.min() < -ampl/2+off: 1003 | raise ValueError('Waveform voltages exceed channel range ' 1004 | 'on channel {}'.format(chan) + 1005 | ' sequence element {}. '.format(pos) + 1006 | '{} < {}!'.format(wfm.min(), -ampl/2+off)) 1007 | wfm = rescaler(wfm, ampl, off) 1008 | element[chan]['wfm'] = wfm 1009 | elements[pos-1] = element 1010 | 1011 | # Finally cast the lists into the shapes required by the AWG driver 1012 | waveforms = [[] for dummy in range(len(channels))] 1013 | m1s = [[] for dummy in range(len(channels))] 1014 | m2s = [[] for dummy in range(len(channels))] 1015 | nreps = [] 1016 | trig_waits = [] 1017 | gotos = [] 1018 | jump_tos = [] 1019 | 1020 | # Since sequencing options are valid/invalid differently for 1021 | # different backends, we make the validation here 1022 | for pos in range(1, seqlen+1): 1023 | for chanind, chan in enumerate(channels): 1024 | waveforms[chanind].append(elements[pos-1][chan]['wfm']) 1025 | m1s[chanind].append(elements[pos-1][chan]['m1']) 1026 | m2s[chanind].append(elements[pos-1][chan]['m2']) 1027 | 1028 | twait = self._sequencing[pos]['twait'] 1029 | nrep = self._sequencing[pos]['nrep'] 1030 | jump_to = self._sequencing[pos]['jump_target'] 1031 | goto = self._sequencing[pos]['goto'] 1032 | 1033 | if twait not in [0, 1]: 1034 | raise SequencingError('Invalid trigger wait state at position' 1035 | '{}: {}. Must be either 0 or 1.' 1036 | ''.format(pos, twait)) 1037 | 1038 | if nrep not in range(0, 65537): 1039 | raise SequencingError('Invalid number of repetions at position' 1040 | '{}: {}. Must be either 0 (infinite) ' 1041 | 'or 1-65,536.'.format(pos, nrep)) 1042 | 1043 | if jump_to not in range(-1, seqlen+1): 1044 | raise SequencingError('Invalid event jump target at position' 1045 | '{}: {}. Must be either -1 (next),' 1046 | ' 0 (off), or 1-{}.' 1047 | ''.format(pos, jump_to, seqlen)) 1048 | 1049 | if goto not in range(0, seqlen+1): 1050 | raise SequencingError('Invalid goto target at position' 1051 | '{}: {}. Must be either 0 (next),' 1052 | ' or 1-{}.' 1053 | ''.format(pos, goto, seqlen)) 1054 | 1055 | trig_waits.append(twait) 1056 | nreps.append(nrep) 1057 | jump_tos.append(jump_to) 1058 | gotos.append(goto) 1059 | 1060 | # ...and make a sliceable object out of them 1061 | output = _AWGOutput((waveforms, m1s, m2s, nreps, 1062 | trig_waits, gotos, 1063 | jump_tos), self.channels) 1064 | 1065 | return output 1066 | --------------------------------------------------------------------------------