diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml
index 8d56cff3c..da7ebdbdd 100644
--- a/.github/workflows/pythonpublish.yml
+++ b/.github/workflows/pythonpublish.yml
@@ -1,26 +1,48 @@
-name: Upload Python Package
+name: Publish Python Package
on:
+ workflow_dispatch: {}
release:
- types: [created]
+ types: [published]
jobs:
- deploy:
+ build:
+ if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'release' }}
+ name: Build release artifacts
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v1
- - name: Set up Python
- uses: actions/setup-python@v1
- with:
- python-version: '3.8'
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- python -m pip install --upgrade build twine
- - name: Build and publish
- env:
- TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
- TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
- run: |
- python -m build
- python -m twine upload dist/*
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+ - name: Install build tool
+ run: python3 -m pip install --upgrade build
+ - name: Build wheel & sdist
+ run: python3 -m build
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: dist-files
+ path: dist/
+
+ test-publish:
+ if: ${{ github.event_name == 'workflow_dispatch' }}
+ needs: build
+ uses: ./.github/workflows/upload_to_repository.yml
+ with:
+ repository-url: https://test.pypi.org/legacy/
+ secrets:
+ pypi-token: ${{ secrets.TEST_PYPI_API_TOKEN }}
+
+ publish:
+ if: ${{ github.event_name == 'release' }}
+ needs: build
+ uses: ./.github/workflows/upload_to_repository.yml
+ with:
+ repository-url: https://upload.pypi.org/legacy/
+ secrets:
+ pypi-token: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/.github/workflows/pythontest.yaml b/.github/workflows/pythontest.yaml
index cacee5647..a8d5ab833 100644
--- a/.github/workflows/pythontest.yaml
+++ b/.github/workflows/pythontest.yaml
@@ -11,10 +11,10 @@ on:
- '**'
paths:
- 'qupulse/**y'
- - 'qctoolkit/**'
- 'tests/**'
- 'setup.*'
- 'pyproject.toml'
+ - '.github/workflows/*'
jobs:
test:
@@ -22,35 +22,30 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["3.7", "3.8", "3.9"]
- time-type: ["fractions", "gmpy2"]
+ python-version: ["3.10", "3.11", "3.12"]
+ numpy-version: [">=1.24,<2.0", ">=2.0"]
env:
INSTALL_EXTRAS: tests,plotting,zurich-instruments,tektronix,tabor-instruments
steps:
- - name: Prepare gmpy2 build dependencies
- if: ${{ matrix.time-type }} == 'gmpy2'
- run: |
- sudo apt-get install -y libgmp-dev libmpfr-dev libmpc-dev
- echo "INSTALL_EXTRAS=${{ env.INSTALL_EXTRAS }},Faster-fractions" >> $GITHUB_ENV
-
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- # supported since 2.3
cache: pip
- cache-dependency-path: setup.cfg
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install coverage coveralls
+ - name: Install numpy ${{ matrix.numpy-version }}
+ run: python -m pip install "numpy${{ matrix.numpy-version }}"
+
- name: Install package
run: |
python -m pip install .[${{ env.INSTALL_EXTRAS }}]
@@ -59,18 +54,31 @@ jobs:
run: |
coverage run -m pytest --junit-xml pytest.xml
+ - name: Generate valid name
+ run: |
+ numpy_version="${{ matrix.numpy-version }}"
+ if [[ $numpy_version == *"<2"* ]]; then
+ numpy_version="1"
+ else
+ numpy_version="2"
+ fi
+ MATRIX_NAME="python-${{ matrix.python-version }}-numpy-"$numpy_version
+ echo "MATRIX_NAME=$MATRIX_NAME" >> $GITHUB_ENV
+
- name: Upload coverage data to coveralls.io
run: coveralls --service=github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- COVERALLS_FLAG_NAME: python-${{ matrix.python-version }}-${{ matrix.time-type }}
+ COVERALLS_FLAG_NAME: ${{ env.MATRIX_NAME }}
COVERALLS_PARALLEL: true
+ # this step can fail
+ continue-on-error: true
- name: Upload Test Results
if: always()
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v4
with:
- name: Unit Test Results ( ${{ matrix.python-version }}-${{ matrix.time-type }} )
+ name: Unit Test Results ( ${{ env.MATRIX_NAME }} )
path: |
pytest.xml
@@ -86,13 +94,15 @@ jobs:
coveralls --service=github --finish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # this step can fail
+ continue-on-error: true
event_file:
name: "Event File"
runs-on: ubuntu-latest
steps:
- name: Upload
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v4
with:
name: Event File
path: ${{ github.event_path }}
diff --git a/.github/workflows/upload_to_repository.yml b/.github/workflows/upload_to_repository.yml
new file mode 100644
index 000000000..d774d6c66
--- /dev/null
+++ b/.github/workflows/upload_to_repository.yml
@@ -0,0 +1,31 @@
+name: Upload artifacts
+
+on:
+ workflow_call:
+ inputs:
+ repository-url:
+ required: true
+ type: string
+ secrets:
+ pypi-token:
+ required: true
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.13'
+ - uses: actions/checkout@v4
+ - uses: actions/download-artifact@v4
+ with:
+ name: dist-files
+ path: dist/
+ - name: Install Twine
+ run: python3 -m pip install --upgrade twine
+ - name: Upload
+ env:
+ TWINE_USERNAME: __token__
+ TWINE_PASSWORD: ${{ secrets.pypi-token }}
+ run: python -m twine upload --repository-url ${{ inputs.repository-url }} dist/*
diff --git a/.gitignore b/.gitignore
index 7124aaa3e..d7536efc2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,8 +9,8 @@ dist/*
doc/source/examples/.ipynb_checkpoints/*
**.asv
*.orig
-MATLAB/+qc/personalPaths.mat
/doc/source/_autosummary/*
.idea/
.mypy_cache/*
tests/hardware/WX2184C.exe
+.vscode/*
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index df66b4e0b..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,40 +0,0 @@
-language: python
-python:
- - 3.7
- - 3.8
-env:
- - INSTALL_EXTRAS=[plotting,zurich-instruments,tektronix,tabor-instruments]
- - INSTALL_EXTRAS=[plotting,zurich-instruments,tektronix,tabor-instruments,Faster-fractions,faster-sampling]
-
-#use container based infrastructure
-sudo: false
-
-#these directories are persistent
-cache: pip
-
-# install dependencies for gmpy2
-addons:
- apt:
- update: true
-
- sources:
- # newer compiler for zhinst
- - ubuntu-toolchain-r-test
-
- packages:
- - libgmp-dev
- - libmpfr-dev
- - libmpc-dev
-
-before_install:
- - eval "CC=gcc-8 && GXX=g++-8"
- - pip install coverage coveralls
-install:
- - pip install .$INSTALL_EXTRAS
-script:
- - "coverage run --source=qupulse --rcfile=coverage.ini setup.py test"
-after_success:
- - coveralls
-
-notifications:
- email: false
diff --git a/.zenodo.json b/.zenodo.json
new file mode 100644
index 000000000..69d6c7d28
--- /dev/null
+++ b/.zenodo.json
@@ -0,0 +1,74 @@
+{
+ "creators": [
+ {
+ "orcid": "0000-0002-9399-1055",
+ "affiliation": "RWTH Aachen University",
+ "name": "Humpohl, Simon"
+ },
+ {
+ "orcid": "0000-0001-8678-961X",
+ "affiliation": "RWTH Aachen University",
+ "name": "Prediger, Lukas"
+ },
+ {
+ "orcid": "0000-0002-8227-4018",
+ "affiliation": "RWTH Aachen University",
+ "name": "Cerfontaine, Pascal"
+ },
+ {
+ "affiliation": "Forschungszentrum Jülich",
+ "name": "Papajewski, Benjamin"
+ },
+ {
+ "orcid": "0000-0001-9927-3102",
+ "affiliation": "RWTH Aachen University",
+ "name": "Bethke, Patrick"
+ },
+ {
+ "orcid": "0000-0003-2057-9913",
+ "affiliation": "Forschungszentrum Jülich",
+ "name": "Lankes, Lukas"
+ },
+ {
+ "orcid": "0009-0006-9702-2979",
+ "affiliation": "Forschungszentrum Jülich",
+ "name": "Willmes, Alexander"
+ },
+ {
+ "orcid": "0009-0000-3779-4711",
+ "affiliation": "Forschungszentrum Jülich",
+ "name": "Kammerloher, Eugen"
+ }
+ ],
+
+ "contributors": [
+ {
+ "orcid": "0000-0001-7018-1124",
+ "affiliation": "Netherlands Organisation for Applied Scientific Research TNO",
+ "name": "Eendebak, Pieter Thijs"
+ },
+ {
+ "name": "Kreutz, Maike",
+ "affiliation": "RWTH Aachen University"
+ },
+ {
+ "name": "Xue, Ran",
+ "affiliation": "RWTH Aachen University",
+ "orcid": "0000-0002-2009-6279"
+ }
+ ],
+
+ "related_identifiers": [
+ {
+ "identifier": "2128/24264",
+ "relation": "isDocumentedBy",
+ "resource_type": "publication-thesis"
+ }
+ ],
+
+ "license": "GPL-3.0-or-later",
+
+ "title": "qupulse: A Quantum compUting PULse parametrization and SEquencing framework",
+
+ "keywords": ["quantum computing", "control pulse"]
+}
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index a0b058a8e..000000000
--- a/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2018 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/LICENSES/LGPL-3.0-or-later.txt b/LICENSES/LGPL-3.0-or-later.txt
new file mode 100644
index 000000000..65c5ca88a
--- /dev/null
+++ b/LICENSES/LGPL-3.0-or-later.txt
@@ -0,0 +1,165 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/MATLAB/+qc/AWGwatch.m b/MATLAB/+qc/AWGwatch.m
deleted file mode 100644
index 79e7a80db..000000000
--- a/MATLAB/+qc/AWGwatch.m
+++ /dev/null
@@ -1,21 +0,0 @@
-function AWGwatch()
-% starts a matlab (vers 2018a) app to DISPLAY SEQUENCER TABLES AND
-% WAFEFORMS in qctoolkit and on the Tabor AWG simulatar
-% -------------------------------------------------------------------------
-% - to edit the app open awgdisp_app.mlapp in the Matlab app designer
-% - user preferences can be edited in the app private properties directly
-% at the top in awgdisp_app.mlapp
-% -------------------------------------------------------------------------
-% App written by Marcel Meyer 08|2018 marcel.meyer1@rwth-aachen.de
-
- disp('AWGwatch - app is started');
-
- % the app is not on path +qc because then one has problems debugging it
- pathOfApp = which('qc.AWGwatch');
- pathOfApp = pathOfApp(1:end-10);
- pathOfApp = [pathOfApp 'AWGwatch\'];
- addpath(pathOfApp);
-
- awgdisp_app();
-
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/AWGwatch/awgdisp_app.mlapp b/MATLAB/+qc/AWGwatch/awgdisp_app.mlapp
deleted file mode 100644
index 25de9342c..000000000
Binary files a/MATLAB/+qc/AWGwatch/awgdisp_app.mlapp and /dev/null differ
diff --git a/MATLAB/+qc/add_params_to_dict.m b/MATLAB/+qc/add_params_to_dict.m
deleted file mode 100644
index 64d15b5c5..000000000
--- a/MATLAB/+qc/add_params_to_dict.m
+++ /dev/null
@@ -1,26 +0,0 @@
-function d = add_params_to_dict(d, parameters, pulse_name)
-
- if nargin < 3
- pulse_name= [];
- end
-
- delim = '___';
-
- fn = fieldnames(parameters)';
-
- if ~isempty(fn) && util.str_contains(fn{1}, delim)
- [parameters, extracted_pulse_name] = qc.params_rm_delim(parameters);
-
- if isempty(pulse_name)
- pulse_name = extracted_pulse_name;
- end
- end
-
- if isempty(pulse_name)
- error('Pulse name must not be empty');
- end
-
- d = qc.load_dict(d);
- d.(pulse_name) = parameters;
-
-
diff --git a/MATLAB/+qc/array2list.m b/MATLAB/+qc/array2list.m
deleted file mode 100644
index 9865457fb..000000000
--- a/MATLAB/+qc/array2list.m
+++ /dev/null
@@ -1,9 +0,0 @@
-function sOut = array2list(sIn)
- if isstruct(sIn)
- sOut = structfun(@qc.array2list, sIn, 'UniformOutput', false);
- elseif isnumeric(sIn) && ~isscalar(sIn)
- sOut = py.list(sIn(:).');
- else
- sOut = sIn;
- end
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/array2row.m b/MATLAB/+qc/array2row.m
deleted file mode 100644
index f82e19a7d..000000000
--- a/MATLAB/+qc/array2row.m
+++ /dev/null
@@ -1,9 +0,0 @@
-function sOut = array2row(sIn)
- if isstruct(sIn)
- sOut = structfun(@qc.array2row, sIn, 'UniformOutput', false);
- elseif isnumeric(sIn) && ~isscalar(sIn)
- sOut = sIn(:).';
- else
- sOut = sIn;
- end
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/awg_program.m b/MATLAB/+qc/awg_program.m
deleted file mode 100644
index 91b06086c..000000000
--- a/MATLAB/+qc/awg_program.m
+++ /dev/null
@@ -1,320 +0,0 @@
-function [program, bool, msg] = awg_program(ctrl, varargin)
- % pulse_template can also be a pulse name. In that case the pulse is
- % automatically loaded.
-
- global plsdata
- hws = plsdata.awg.hardwareSetup;
- daq = plsdata.daq.inst;
-
- program = struct();
- msg = '';
- bool = false;
-
- default_args = struct(...
- 'program_name', 'default_program', ...
- 'pulse_template', 'default_pulse', ...
- 'parameters_and_dicts', {plsdata.awg.defaultParametersAndDicts}, ...
- 'channel_mapping', plsdata.awg.defaultChannelMapping, ...
- 'window_mapping', plsdata.awg.defaultWindowMapping, ...
- 'global_transformation', plsdata.awg.globalTransformation, ...
- 'add_marker', {plsdata.awg.defaultAddMarker}, ...
- 'force_update', false, ...
- 'verbosity', 10 ...
- );
- a = util.parse_varargin(varargin, default_args);
-
- % --- add ---------------------------------------------------------------
- if strcmp(ctrl, 'add')
- [~, bool, msg] = qc.awg_program('fresh', qc.change_field(a, 'verbosity', 0));
- if ~bool || a.force_update
- plsdata.awg.currentProgam = '';
-
- % Deleting old program should not be necessary. In practice however,
- % updating an existing program seemed to crash Matlab sometimes.
- % qc.awg_program('remove', qc.change_field(a, 'verbosity', 10));
-
- a.pulse_template = pulse_to_python(a.pulse_template);
- [a.pulse_template, a.channel_mapping] = add_marker_if_not_empty(a.pulse_template, a.add_marker, a.channel_mapping);
-
- program = qc.program_to_struct(a.program_name, a.pulse_template, a.parameters_and_dicts, a.channel_mapping, a.window_mapping, a.global_transformation);
- plsdata.awg.registeredPrograms.(a.program_name) = program;
-
- % Save AWG amplitude at instantiation and upload time so that the
- % amplitude at the sample can be reconstructed at a later time
- if ~isfield(plsdata.awg.registeredPrograms.(a.program_name), 'amplitudes_at_upload')
- % program not online yet
- plsdata.awg.registeredPrograms.(a.program_name).amplitudes_at_upload = zeros(1, 4);
- end
-
- for ii = int64(1:4)
- % query actual amplitude from qupulse
- plsdata.awg.registeredPrograms.(a.program_name).amplitudes_at_upload(ii) = plsdata.awg.inst.amplitude(ii);
- end
-
- if a.verbosity > 9
- fprintf('Program ''%s'' is now being instantiated...', a.program_name);
- tic;
- end
- instantiated_pulse = qc.instantiate_pulse(a.pulse_template, 'parameters', qc.join_params_and_dicts(program.parameters_and_dicts), 'channel_mapping', program.channel_mapping, 'window_mapping', program.window_mapping, 'global_transformation', program.global_transformation);
-
- if a.verbosity > 9
- fprintf('took %.0fs\n', toc);
- fprintf('Program ''%s'' is now being uploaded...', a.program_name);
- tic
- end
- util.py.call_with_interrupt_check(py.getattr(hws, 'register_program'), program.program_name, instantiated_pulse, pyargs('update', py.True));
-
- if a.verbosity > 9
- fprintf('took %.0fs\n', toc);
- end
-
- if bool && a.force_update
- msg = ' since update forced';
- else
- msg = '';
- end
- msg = sprintf('Program ''%s'' added%s', a.program_name, msg);
-
- bool = true;
- else
- program = plsdata.awg.registeredPrograms.(a.program_name);
- end
-
- % --- arm ---------------------------------------------------------------
- elseif strcmp(ctrl, 'arm')
- % Call directly before trigger comes, otherwise you might encounter a
- % trigger timeout. Also, call after daq_operations('add')!
- [~, bool, msg] = qc.awg_program('present', qc.change_field(a, 'verbosity', 0));
- if bool
- % Wait for AWG to stop playing pulse, otherwise this might lead to a
- % trigger timeout since the DAQ is not necessarily configured for the
- % whole pulse time and can return data before the AWG stops playing
- % the pulse.
- if ~isempty(plsdata.awg.currentProgam)
- waitingTime = min(max(plsdata.awg.registeredPrograms.(plsdata.awg.currentProgam).pulse_duration + plsdata.awg.registeredPrograms.(plsdata.awg.currentProgam).added_to_pulse_duration - (now() - plsdata.awg.triggerStartTime)*24*60*60, 0), plsdata.awg.maxPulseWait);
- if waitingTime == plsdata.awg.maxPulseWait
- warning('Maximum waiting time ''plsdata.awg.maxPulseWait'' = %g s reached.\nIncrease if you experience problems with the data acquistion.', plsdata.awg.maxPulseWait);
- end
- pause(waitingTime);
- % fprintf('Waited for %.3fs for pulse to complete\n', waitingTime);
- end
-
- % No longer needed since bug has been fixed
- % qc.workaround_4chan_program_errors(a);
-
- hws.arm_program(a.program_name);
-
- plsdata.awg.currentProgam = a.program_name;
- bool = true;
- msg = sprintf('Program ''%s'' armed', a.program_name);
- end
-
- % --- arm ---------------------------------------------------------------
- elseif strcmp(ctrl, 'arm global')
- if ischar(plsdata.awg.armGlobalProgram)
- globalProgram = plsdata.awg.armGlobalProgram;
- elseif iscell(plsdata.awg.armGlobalProgram)
- globalProgram = plsdata.awg.armGlobalProgram{1};
- plsdata.awg.armGlobalProgram = circshift(plsdata.awg.armGlobalProgram, -1);
- else
- globalProgram = a.program_name;
- warning('Not using global program since plsdata.awg.armGlobalProgram must contain a char or a cell.');
- end
-
- % Set scan axis labels here if the global program armament is called by
- % a prefn in smrun. Only for charge scans
- if startsWith(globalProgram, 'charge_4chan')
- f = figure(a.fig_id);
-
- % always query the rf channels being swept so that they can be logged
- % by a metafn.
- if startsWith(globalProgram, 'charge_4chan_d12')
- idx = [1 2];
- chans = {'A' 'B'};
- elseif startsWith(globalProgram, 'charge_4chan_d23')
- idx = [2 3];
- chans = {'B' 'C'};
- elseif startsWith(globalProgram, 'charge_4chan_d34')
- idx = [4 3];
- chans = {'D' 'C'};
- elseif startsWith(globalProgram, 'charge_4chan_d14')
- idx = [1 4];
- chans = {'A' 'D'};
- end
- plsdata.awg.currentChannels = chans;
-
- % compare current AWG channel amplitudes to those at instantiation
- % time
- currentAmplitudes = plsdata.awg.currentAmplitudesHV;
- uploadAmplitudes = plsdata.awg.registeredPrograms.(globalProgram).amplitudes_at_upload;
-
- updateRFchans = ~strcmp(globalProgram, a.program_name);
- updateRFamps = ~all(currentAmplitudes == uploadAmplitudes);
-
- if updateRFchans || updateRFamps
-
- if updateRFamps
- % Calculate amplitude at sample from the current amplitude, the
- % amplitude at pulse instantiation time, and the pulse parameters
- rng = [(plsdata.awg.registeredPrograms.(globalProgram).parameters_and_dicts{2}.charge_4chan___stop_x - ...
- plsdata.awg.registeredPrograms.(globalProgram).parameters_and_dicts{2}.charge_4chan___start_x) ...
- (plsdata.awg.registeredPrograms.(globalProgram).parameters_and_dicts{2}.charge_4chan___stop_y - ...
- plsdata.awg.registeredPrograms.(globalProgram).parameters_and_dicts{2}.charge_4chan___start_y)];
- amps = currentAmplitudes(idx)./uploadAmplitudes(idx).*rng*1e3;
- end
- for ax = f.Children(2:2:end)'
- % Don't use xlabel(), ylabel() to stop matlab from updating the rest of the figure
- if updateRFchans
- ax.XLabel.String(3) = chans{1};
- ax.YLabel.String(3) = chans{2};
- end
- if updateRFamps
- ax.XLabel.String(6:9) = sprintf('%.1f', amps(1));
- ax.YLabel.String(6:9) = sprintf('%.1f', amps(2));
- ax.XTick = 0:10:100;
- ax.YTick = 0:10:100;
- ax.XTickLabel = sprintfc('%.1f', linspace(-amps(1)/2, amps(1)/2, 11));
- ax.YTickLabel = sprintfc('%.1f', linspace(-amps(2)/2, amps(2)/2, 11));
- end
- end
- end
- end
-% This code outputs the wrong pulses and isn't even faster
-% - Then why is it still here? - TH
-% registered_programs = util.py.py2mat(py.getattr(hws,'_registered_programs'));
-% program = registered_programs.(globalProgram);
-% awgs_to_upload_to = program{4};
-% dacs_to_arm = program{5};
-% for awgToUploadTo = awgs_to_upload_to
-% awgToUploadTo{1}.arm(globalProgram);
-% end
-% for dacToArm = dacs_to_arm
-% dacToArm{1}.arm_program(plsdata.awg.currentProgam);
-% end
-
- qc.awg_program('arm', 'program_name', globalProgram, 'verbosity', a.verbosity, 'arm_global_for_workaround_4chan_program_errors', []);
-
- % --- remove ------------------------------------------------------------
- elseif strcmp(ctrl, 'remove')
- % Arm the idle program so the program to be remove is not active by
- % any chance (should not be needed - please test more thorougly whether it is needed)
- plsdata.awg.inst.channel_pair_AB.arm(py.None);
- plsdata.awg.inst.channel_pair_CD.arm(py.None);
-
- [~, bool, msg] = qc.awg_program('present', qc.change_field(a, 'verbosity', 0));
-
- if bool
- bool = false;
-
- if isfield(plsdata.awg.registeredPrograms, a.program_name)
- plsdata.awg.registeredPrograms = rmfield(plsdata.awg.registeredPrograms, a.program_name);
- end
-
- try
- hws.remove_program(a.program_name);
- bool = true;
- catch err
- warning('The following error was encountered when running hardware_setup.remove_program.\nPlease debug AWG commands.\nThis might have to do with removing the current program.\n.Trying to recover by deleting operations.\n%s', err.getReport());
- qc.daq_operations('remove', 'program_name', a.program_name, 'verbosity', 10);
- end
-
- msg = sprintf('Program ''%s'' removed', a.program_name);
- end
-
- % --- clear all ---------------------------------------------------------
- elseif strcmp(ctrl, 'clear all') % might take a long time
- plsdata.awg.registeredPrograms = struct();
- program_names = fieldnames(util.py.py2mat(py.getattr(hws, '_registered_programs')));
-
- bool = true;
- for program_name = program_names.'
- [~, boolNew] = qc.awg_program('remove', 'program_name', program_name{1}, 'verbosity', 10);
- bool = bool & boolNew;
- end
-
- if bool
- msg = 'All programs cleared';
- else
- msg = 'Error when trying to clear all progams';
- end
-
- % --- clear all fast ----------------------------------------------------
- elseif strcmp(ctrl, 'clear all fast') % fast but need to clear awg manually
- hws.registered_programs.clear();
- py.getattr(daq, '_registered_programs').clear();
-
- % --- present -----------------------------------------------------------
- elseif strcmp(ctrl, 'present') % returns true if program is present
- bool = py.list(hws.registered_programs.keys()).count(a.program_name) ~= 0;
- if bool
- msg = '';
- else
- msg = 'not ';
- end
- msg = sprintf('Program ''%s'' %spresent', a.program_name, msg);
-
- % --- fresh -------------------------------------------------------------
- elseif strcmp(ctrl, 'fresh') % returns true if program is present and has not changed
- [~, bool, msg] = qc.awg_program('present', qc.change_field(a, 'verbosity', 0));
-
- if isfield(plsdata.awg.registeredPrograms, a.program_name) && bool
- a.pulse_template = pulse_to_python(a.pulse_template);
- [a.pulse_template, a.channel_mapping] = add_marker_if_not_empty(a.pulse_template, a.add_marker, a.channel_mapping);
-
- newProgram = qc.program_to_struct(a.program_name, a.pulse_template, a.parameters_and_dicts, a.channel_mapping, a.window_mapping, a.global_transformation);
- newProgram = qc.get_minimal_program(newProgram);
-
- awgProgram = plsdata.awg.registeredPrograms.(a.program_name);
- awgProgram = qc.get_minimal_program(awgProgram);
-
- bool = isequal(newProgram, awgProgram);
-
- if bool
- msg = '';
- else
- msg = 'not ';
- end
- msg = sprintf('Program ''%s'' is %sup to date (fresh)', a.program_name, msg);
- end
- % if ~bool
- % util.comparedata(newProgram, awgProgram);
- % end
-
- end
-
- if a.verbosity > 9
- fprintf([msg '\n']);
- end
-
-
-
-
-function pulse_template = pulse_to_python(pulse_template)
-
- if ischar(pulse_template)
- pulse_template = qc.load_pulse(pulse_template);
- end
-
- if isstruct(pulse_template)
- pulse_template = qc.struct_to_pulse(pulse_template);
- end
-
-
-function [pulse_template, channel_mapping] = add_marker_if_not_empty(pulse_template, add_marker, channel_mapping)
-
- if ~iscell(add_marker)
- add_marker = {add_marker};
- end
-
- if ~isempty(add_marker)
- marker_pulse = py.qctoolkit.pulses.PointPT({{0, 1},...
- {py.getattr(pulse_template, 'duration'), 1}}, add_marker);
- pulse_template = py.qctoolkit.pulses.AtomicMultiChannelPT(pulse_template, marker_pulse);
-
- for ii = 1:numel(add_marker)
- channel_mapping.(args.add_marker{ii}) = add_marker{ii};
- end
- end
-
-
-
\ No newline at end of file
diff --git a/MATLAB/+qc/awgdisp.m b/MATLAB/+qc/awgdisp.m
deleted file mode 100644
index f92b16727..000000000
--- a/MATLAB/+qc/awgdisp.m
+++ /dev/null
@@ -1,20 +0,0 @@
-function awgdisp(source)
-
-if nargin < 1
- source = 'qctk';
-end
-
-% get AWG objects
-switch source
- case 'qctk'
- disp('source = qctoolkit')
- case 'sim'
- disp('simulaotr')
- otherwise
- disp('no input')
-end
-
-
-% get sequence tables
-
-% create app
\ No newline at end of file
diff --git a/MATLAB/+qc/change_armed_program.m b/MATLAB/+qc/change_armed_program.m
deleted file mode 100644
index df91085dd..000000000
--- a/MATLAB/+qc/change_armed_program.m
+++ /dev/null
@@ -1,32 +0,0 @@
-function change_armed_program(program_name, turn_awg_off)
-% CHANGE_ARMED_PROGRAM Force arming of a program on Tabor AWG
-% This function calls change_armed_program and each Tabor channel pair
-% which contains the indicated program. The program needs to be already
-% present on the AWG.
-% --- Inputs --------------------------------------------------------------
-% program_name : Program name which is armed
-% turn_awg_off : Turn AWG off after arming the program.
-% Default is true.
-% -------------------------------------------------------------------------
-% (c) 2018/06 Pascal Cerfontaine (cerfontaine@physik.rwth-aachen.de)
-
-global plsdata
-hws = plsdata.awg.hardwareSetup;
-
-if nargin < 2 || isempty(turn_awg_off)
- turn_awg_off = true;
-end
-
-known_awgs = util.py.py2mat(hws.known_awgs);
-
-for k = 1:length(known_awgs)
- known_programs{k} = util.py.py2mat(py.getattr(known_awgs{k}, '_known_programs'));
-
- if isfield(known_programs{k}, program_name)
- known_awgs{k}.change_armed_program(program_name);
- end
-end
-
-if turn_awg_off
- awgctrl('off');
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/change_field.m b/MATLAB/+qc/change_field.m
deleted file mode 100644
index dd22675eb..000000000
--- a/MATLAB/+qc/change_field.m
+++ /dev/null
@@ -1,3 +0,0 @@
-function s = change_field(s, fieldName, value)
- s.(fieldName) = value;
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/check_pulse_parameter_dependency.m b/MATLAB/+qc/check_pulse_parameter_dependency.m
deleted file mode 100644
index e8d281dbf..000000000
--- a/MATLAB/+qc/check_pulse_parameter_dependency.m
+++ /dev/null
@@ -1,64 +0,0 @@
-function check_pulse_parameter_dependency(pulse, check_parameter, check_values, varargin)
-% plot function to visualize the influence of one pulse parameter on the
-% pulse shape:
-% ----------------------------------------------------------------------
-% input:
-% pulse : loaded qctoolkit pulse
-% check_paramter : name of the paramter under investigation
-% check_values : values the parameter is set to, array []
-% varargin : standard varargin that one also uses for
-% qc.plot_pulse
-% ----------------------------------------------------------------------
-% written by Marcel Meyer 08|2018 (marcel.meyer1@rwth-aachen.de)
-
-global plsdata
-
-defaultArgs = struct(...
- 'sample_rate', plsdata.awg.sampleRate, ... % in 1/s, converted to 1/ns below
- 'channel_mapping', py.None, ...
- 'window_mapping' , py.None, ...
- 'parameters', struct(), ...
- 'removeTrigChans', true, ...
- 'figID', 2018 ...
- );
-
-args = util.parse_varargin(varargin, defaultArgs);
-
-if isempty(args.channel_mapping) || args.channel_mapping == py.None
-
- args.channel_mapping = py.dict(py.zip(pulse.defined_channels, pulse.defined_channels));
-end
-
-args.sample_rate = args.sample_rate * 1e-9; % convert to 1/ns
-
-figure(args.figID);
-clf;
-
-for k = 1:numel(check_values)
- args.parameters.(check_parameter) = check_values(k);
-
- instantiatedPulse = qc.instantiate_pulse(pulse, 'parameters', args.parameters, 'channel_mapping', args.channel_mapping, 'window_mapping', args.window_mapping);
- data = util.py.py2mat(py.qctoolkit.pulses.plotting.render(instantiatedPulse, pyargs('sample_rate', args.sample_rate, 'render_measurements', true)));
-
- subplot(1, numel(check_values), k);
-
- hold on;
-
- if args.removeTrigChans
- data{2} = rmfield(data{2}, 'MTrig');
- data{2} = rmfield(data{2}, 'M1');
- data{2} = rmfield(data{2}, 'M2');
- end
-
- channelNames = fieldnames(data{2});
-
- for channelInd = 1:numel(channelNames)
- plot(data{1}*1e-9, data{2}.(channelNames{channelInd}));
- end
- hold off;
- legend(channelNames);
- xlabel('t(s)');
- title(sprintf('%s = %.2d', check_parameter, check_values(k)), 'interpreter', 'none');
-end
-
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/cleanupfn_awg.m b/MATLAB/+qc/cleanupfn_awg.m
deleted file mode 100644
index 3e3a8a438..000000000
--- a/MATLAB/+qc/cleanupfn_awg.m
+++ /dev/null
@@ -1,9 +0,0 @@
-function scan = cleanupfn_awg(scan)
-
- if nargin < 1
- scan = [];
- end
-
- evalin('caller', 'cleanupFnAwg = onCleanup(@()({awgctrl(''off''), fprintf(''Executing cleanup function: Turned AWG outputs off\n'')}));');
-
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/cleanupfn_delete_getchans.m b/MATLAB/+qc/cleanupfn_delete_getchans.m
deleted file mode 100644
index 67216b0d4..000000000
--- a/MATLAB/+qc/cleanupfn_delete_getchans.m
+++ /dev/null
@@ -1,7 +0,0 @@
-function scan = cleanupfn_delete_getchans(scan, getchans)
-
- for getchan = getchans
- evalin('caller', sprintf('data{%i} = {};', getchan));
- end
-
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/cleanupfn_rf_sources.m b/MATLAB/+qc/cleanupfn_rf_sources.m
deleted file mode 100644
index 018ba0830..000000000
--- a/MATLAB/+qc/cleanupfn_rf_sources.m
+++ /dev/null
@@ -1,11 +0,0 @@
-function scan = cleanupfn_rf_sources(scan)
-
- if nargin < 1
- scan = [];
- end
-
- evalin('caller', 'cleanupFnRfMsg = onCleanup(@()(fprintf(''Executing cleanup function: Turned RF sources off\n'')));');
- evalin('caller', 'cleanupFnRf1 = onCleanup(@()(smset(''RF1_on'', 0)));');
- evalin('caller', 'cleanupFnRf2 = onCleanup(@()(smset(''RF2_on'', 0)));');
-
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/compensate_channels.m b/MATLAB/+qc/compensate_channels.m
deleted file mode 100644
index ec6252637..000000000
--- a/MATLAB/+qc/compensate_channels.m
+++ /dev/null
@@ -1,90 +0,0 @@
-function [W, X, Y, Z] = compensate_channels(t, W, X, Y, Z, interpolation, comp_param_name)
-% ADD_CHANNELS Compensate linear crosstalk for two ST0 qubits
-% This function adds W and X to Y and Z (and vice-versa) with individual
-% multipliers. It is specifically designed for compensating linear control
-% crosstalk two ST0 qubits but might be suited for other qubit
-% implementations as well.
-%
-% --- Outputs -------------------------------------------------------------
-% W, X : Compensated qubit 1 channel values (cell)
-% Y, Z : Compensated qubit 2 channel values (cell)
-%
-% --- Inputs --------------------------------------------------------------
-% t, W, X, Y, Z, interpolation, MTrig, M1, M2 are cells with the same
-% number of entries
-%
-% t : Time indices of the channel values (cell)
-% One row: Applied to all channels
-% *Two rows: Row 1 applied to W, X and row 2 to Y, Z
-% *Four rows: One row for each channel
-% W, X : Qubit 1 channel values (cell)
-% Y, Z : Qubit 2 channel values (cell)
-% interpolation : Interpolation strategies, use ‘hold’ for default
-% qupulse behaviour
-% One row: Applied to all channels
-% *Two rows: Row 1 applied to W, X and row 2 to Y, Z
-% *Four rows: One row for each channel
-% comp_param_name: Name of the compensation parameters
-%
-% * Currently disabled since compensation might not work if using
-% different times
-%
-% -------------------------------------------------------------------------
-% (c) 2018/05 Pascal Cerfontaine (cerfontaine@physik.rwth-aachen.de)
-
- assert(all(numel(t) == cellfun(@numel, {W, X, Y, Z, interpolation})), 'Cell input arguments must have the same number of elements');
- assert(size(t, 1) == 1 && size(interpolation, 1) == 1, 'Compensation of different times and interpolation strategies not currently supported');
-
- if nargin < 7 || isempty(comp_param_name)
- comp_param_name = 'globals___comp';
- end
-
- if ~strcmp(comp_param_name, 'compensation_off')
- [W, X, Y, Z] = comp_channels(W, X, Y, Z, comp_param_name);
- end
- [W, X, Y, Z] = format_channels(t, interpolation, W, X, Y, Z);
-
-end
-
-
-function [W, X, Y, Z] = comp_channels(W, X, Y, Z, comp_param_name)
- for k = 1:numel(W)
- Wk = W{k};
- Xk = X{k};
-
- W{k} = [W{k} ' + ' comp_param_name '_w_y*( ' Y{k} ' ) + ' comp_param_name '_w_z*( ' Z{k} ' )'];
- X{k} = [X{k} ' + ' comp_param_name '_x_y*( ' Y{k} ' ) + ' comp_param_name '_x_z*( ' Z{k} ' )'];
-
- Y{k} = [Y{k} ' + ' comp_param_name '_y_w*( ' Wk ' ) + ' comp_param_name '_y_x*( ' Xk ' )'];
- Z{k} = [Z{k} ' + ' comp_param_name '_z_w*( ' Wk ' ) + ' comp_param_name '_z_x*( ' Xk ' )'];
- end
-
-end
-
-function varargout = format_channels(t, interpolation, varargin)
-
- varargout = cell(numel(varargin), 1);
-
- if size(interpolation, 1) == 1
- interpolation = repmat(interpolation, 4, 1);
- elseif size(interpolation, 1) == 2
- interpolation = [ repmat(interpolation(1, :), 2, 1) ;
- repmat(interpolation(2, :), 2, 1) ];
- end
-
- if size(t, 1) == 1
- t = repmat(t, 4, 1);
- elseif size(t, 1) == 2
- t = [ repmat(t(1, :), 2, 1) ;
- repmat(t(2, :), 2, 1) ];
- end
-
- for k = 1:size(t, 2)
- for v = 1:numel(varargin)
-
- varargout{v}{end+1} = { t{v, k}, varargin{v}{k}, interpolation{v, k} };
-
- end
- end
-
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/conf_seq.m b/MATLAB/+qc/conf_seq.m
deleted file mode 100644
index b58afab43..000000000
--- a/MATLAB/+qc/conf_seq.m
+++ /dev/null
@@ -1,326 +0,0 @@
-function scan = conf_seq(varargin)
- % CONF_SEQ Create special-measure scans with inline qctoolkit pulses
- %
- % Only supports inline scans at the moment (could in principle arm a
- % different program in each loop iteration using prefns but this is not
- % implemented at the moment).
- %
- % Please only add aditional configfns directly before turning the AWG on
- % since some other programs fetch information using configfn indices.
- %
- % This function gets only underscore arguments to be more consistend with
- % qctoolkit. Other variables in this function are camel case.
- %
- % --- Outputs -------------------------------------------------------------
- % scan : special-measure scan
- %
- % --- Inputs --------------------------------------------------------------
- % varargin : name-value pairs or parameter struct. For a list of
- % parameters see the struct defaultArgs below.
- %
- % -------------------------------------------------------------------------
- % (c) 2018/02 Pascal Cerfontaine (cerfontaine@physik.rwth-aachen.de)
-
- global plsdata
-
- alazarName = plsdata.daq.instSmName;
-
- % None of the arguments except pulse_template should contain any python
- % objects to avoid erroneous saving when the scan is executed.
- defaultArgs = struct(...
- ... Pulses
- 'program_name', 'default_program', ...
- 'pulse_template', 'default_pulse', ...
- 'parameters_and_dicts', {plsdata.awg.defaultParametersAndDicts}, ...
- 'channel_mapping', plsdata.awg.defaultChannelMapping, ...
- 'window_mapping', plsdata.awg.defaultWindowMapping, ...
- 'add_marker', {plsdata.awg.defaultAddMarker}, ...
- 'force_update', false, ...
- ...
- ... Pulse modification
- 'pulse_modifier_args', struct(), ... % Additional arguments passed to the pulse_modifier_fn
- 'pulse_modifier', false, ... % Automatically change the variable a (all input arguments) below, can be used to dynamically modify the pulse
- 'pulse_modifier_fn', @tune.add_dbz_fid, ... % Can specify a custom function here which modifies the variable a (all input arguments) below
- ...
- ... Saving variables
- 'save_custom_var_fn', @tune.get_global_opts,... % Can specify a function which returns data to be saved in the scan
- 'save_custom_var_args', {{'dnp', 'tune_gui'}}, ...
- 'save_metadata_fns', {{@sm_scans.triton_200.metafn_get_configchanvals} ... % Can specify functions to log metadata during each loop
- {@sm_scans.triton_200.metafn_get_rf_channels}}, ...
- 'save_metadata_fields', {{'configchanvals'} {'rfChannels'}}, ... % Fieldnames of the metadata struct saved by smrun
- ...
- ... Measurements
- 'operations', {plsdata.daq.defaultOperations}, ...
- ...
- ... Other
- 'nrep', 10, ... % Numer of repetition of pulse
- 'fig_id', 2000, ...
- 'fig_position', [], ...
- 'disp_ops', ' default', ... % Refers to operations: List of indices of operations to show
- 'disp_dim', [1 2], ... % dimension of display
- 'delete_getchans', [1], ... % Refers to getchans: Indices of getchans (including those generated by procfns) to delete after the scan is complete
- 'procfn_ops', {{}}, ... % Refers to operations: One entry for each virtual channel, each cell entry has four or five element: fn, args, dim, operation index, (optional) identifier
- 'saveloop', 0, ... % save every nth loop
- 'useCustomCleanupFn', false, ... % If this flag is true
- 'customCleanupFn', [], ... % clean up anything else you would like cleaned up
- 'useCustomConfigFn', false, ... % If this flag is true
- 'customConfigFn', [], ... % add a custom config function which is executed directly before the AWG is turned on
- 'arm_global', false, ... % If true, set the program to be armed via tunedata.global_opts.conf_seq.arm_program_name.
- ... % If you use this, all programs need to be uploaded manually before the scan and need to
- ... % have the same Alazar configuration.
- 'rf_sources', [true true], ... % turn RF sources on and off automatically
- 'buffer_strategy', {plsdata.daq.defaultBufferStrategy},... % call qc.set_alazar_buffer_strategy with these arguments before pulse
- 'verbosity', 10 ... % 0: display nothing, 10: display all except when arming program, 11: display all
- );
- a = util.parse_varargin(varargin, defaultArgs);
- aOriginal = a;
-
- if a.pulse_modifier
- try
- a = feval(a.pulse_modifier_fn, a); % Add any proprietary function here
- catch err
- warning('Could not run pulse_modifier_fn successfully. Continuing as if pulse_modifier was false:\n%s', err.getReport());
- a = aOriginal;
- end
- end
-
- if ~ischar(a.pulse_template) && ~isstruct(a.pulse_template)
- a.pulse_template = qc.pulse_to_struct(a.pulse_template);
- end
-
- if numel(a.rf_sources) == 1
- a.rf_sources = [a.rf_sources a.rf_sources];
- end
-
- scan = struct('configfn', [], 'cleanupfn', [], 'loops', struct('prefn', [], 'metafn', []));
-
- % Save file and arguments with which scan was created (not stricly necessary)
- try
- if ischar(aOriginal.pulse_modifier_fn)
- scan.data.pulse_modifier_fn = fileread(which(aOriginal.pulse_modifier_fn));
- else
- scan.data.pulse_modifier_fn = fileread(which(func2str(aOriginal.pulse_modifier_fn)));
- end
- catch err
- warning('Could not load pulse_modifier_fn for saving in scan for reproducibility:\n%s', err.getReport());
- end
- scan.data.conf_seq_fn = fileread([mfilename('fullpath') '.m']);
- scan.data.conf_seq_args = aOriginal;
-
- % Configure channels
- scan.loops(1).getchan = {'ATSV', 'time'};
- scan.loops(1).setchan = {'count'};
- scan.loops(1).ramptime = [];
- scan.loops(1).npoints = a.nrep;
- scan.loops(1).rng = [];
-
- nGetChan = numel(scan.loops(1).getchan);
- nOperations = numel(a.operations);
-
- % Turn AWG outputs off if scan stops (even if due to error)
- scan.configfn(end+1).fn = @qc.cleanupfn_awg;
- scan.configfn(end).args = {};
-
- % Turn RF sources off if scan stops (even if due to error)
- if any(a.rf_sources)
- scan.configfn(end+1).fn = @qc.cleanupfn_rf_sources;
- scan.configfn(end).args = {};
- end
-
- % Alazar buffer strategy. Can be used to mitigate buffer artifacts.
- scan.configfn(end+1).fn = @smaconfigwrap;
- scan.configfn(end).args = [{@qc.set_alazar_buffer_strategy}, a.buffer_strategy];
-
- % Configure AWG
- % * Calling qc.awg_program('add', ...) makes sure the pulse is uploaded
- % again if any parameters changed.
- % * If dictionaries were passed as strings, this will automatically
- % reload the dictionaries and thus use any changes made in the
- % dictionaries in the meantime.
- % * The original parameters are saved in scan.data.awg_program. This
- % includes the pulse_template in json format and all dictionary
- % entries at the time when the scan was executed.
- % * If a python pulse_template was passed, this will still save
- % correctly since it was converted into a Matlab struct above.
- scan.configfn(end+1).fn = @smaconfigwrap_save_data;
- scan.configfn(end).args = {'awg_program', @qc.awg_program, 'add', a};
-
- % Configure Alazar operations
- % * alazar.update_settings = py.True is automatically set. This results
- % in reconfiguration of the Alazar which takes a long time. Thus this
- % should only be done before a scan is started (i.e. in a configfn).
- % * qc.dac_operations('add', a) also resets the virtual channel in
- % smdata.inst(sminstlookup(alazarName)).data.virtual_channel.
- scan.configfn(end+1).fn = @smaconfigwrap_save_data;
- scan.configfn(end).args = {'daq_operations', @qc.daq_operations, 'add', a};
-
- % Configure Alazar virtual channel
- % * Set datadim of instrument correctly
- % * Save operation lengths in scan.data
- scan.configfn(end+1).fn = @smaconfigwrap_save_data;
- scan.configfn(end).args = {'daq_operations_length', @qc.daq_operations, 'set length', a};
-
- % Extract operation data from first channel ('ATSV')
- % * Add procfns to scan, one for each operation
- % * The configfn qc.conf_seq_procfn sets args and dim of the first n
- % procfns, where n is the number of operations. This ensures that start
- % and stop always use the correct lengths even if they have changed due
- % to changes in pulse dictionaries. qc.conf_seq_procfn assumes that the
- % field scan.data.daq_operations_length has been set dynamically by a
- % previous configfn.
- nGetChan = numel(scan.loops(1).getchan);
- for p = 1:numel(a.operations)
- scan.loops(1).procfn(nGetChan + p).fn(1) = struct( ...
- 'fn', @(x, startInd, stopInd)( x(startInd:stopInd) ), ...
- 'args', {{nan, nan}}, ...
- 'inchan', 1, ...
- 'outchan', nGetChan + p ...
- );
- scan.loops(1).procfn(nGetChan + p).dim = nan;
- end
- scan.configfn(end+1).fn = @qc.conf_seq_procfn;
- scan.configfn(end).args = {};
-
- if any(a.rf_sources)
- % Turn RF switches on
- scan.configfn(end+1).fn = @smaconfigwrap;
- scan.configfn(end).args = {@smset, 'RF1_on', double(a.rf_sources(1))};
- scan.configfn(end+1).fn = @smaconfigwrap;
- scan.configfn(end).args = {@smset, 'RF2_on', double(a.rf_sources(2))};
- scan.configfn(end+1).fn = @smaconfigwrap;
- scan.configfn(end).args = {@pause, 0.05}; % So RF sources definitely on
-
- % Turn RF switches off
- % -> already done by qc.cleanupfn_rf_sources called above
- end
-
- % Add custom variables for documentation purposes
- scan.configfn(end+1).fn = @smaconfigwrap_save_data;
- scan.configfn(end).args = {'custom_var', a.save_custom_var_fn, a.save_custom_var_args};
-
- % Add custom cleanup fn
- if a.useCustomCleanupFn && ~isempty(a.customCleanupFn)
- scan.configfn(end+1).fn = a.customCleanupFn;
- scan.configfn(end).args = {};
- end
-
- % Add custom config fn
- if a.useCustomConfigFn && ~isempty(a.customConfigFn)
- scan.configfn(end+1).fn = a.customConfigFn;
- scan.configfn(end).args = {};
- end
-
- % Delete unnecessary data
- scan.cleanupfn(end+1).fn = @qc.cleanupfn_delete_getchans;
- scan.cleanupfn(end).args = {a.delete_getchans};
-
- % Allow time logging
- % * Update dummy instrument with current time so can get the current time
- % using a getchan
- scan.loops(1).prefn(end+1).fn = @smaconfigwrap;
- scan.loops(1).prefn(end).args = {@(chan)(smset('time', now()))};
-
- % Allow logging metadata
- for i = 1:length(a.save_metadata_fns)
- scan.loops(1).metafn(end+1).fn = @smaconfigwrap_save_metadata;
- scan.loops(1).metafn(end).args = {a.save_metadata_fields{i}, a.save_metadata_fns{i}};
- end
-
- % Turn AWG on
- scan.configfn(end+1).fn = @smaconfigwrap;
- scan.configfn(end).args = {@awgctrl, 'on'};
-
- % Run AWG channel pair 1
- % * Arm the program
- % * Trigger the Alazar
- % * Will later also trigger the RF switches
- % * Will run both channel pairs automatically if they are synced
- % which they should be by default.
- % * Should be the last prefn so no other channels changed when
- % measurement starts (really necessary?)
- scan.loops(1).prefn(end+1).fn = @smaconfigwrap;
- if ~a.arm_global
- scan.loops(1).prefn(end).args = {@qc.awg_program, 'arm', qc.change_field(a, 'verbosity', a.verbosity-1)};
- else
- scan.loops(1).prefn(end).args = {@qc.awg_program, 'arm global', qc.change_field(a, 'verbosity', a.verbosity-1)};
- end
- scan.loops(1).prefn(end+1).fn = @smaconfigwrap;
- scan.loops(1).prefn(end).args = {@awgctrl, 'run', 1};
-
- % Get AWG information (not needed at the moment)
- % [analogNames, markerNames, channels] = qc.get_awg_channels();
- % [programNames, programs] = qc.get_awg_programs();
-
- % Default display
- if strcmp(a.disp_ops, 'default')
- a.disp_ops = 1:min(4, nOperations);
- end
-
- % Add user procfns
- if isfield(scan.loops(1), 'procfn')
- nProcFn = numel(scan.loops(1).procfn);
- else
- nProcFn = 0;
- end
- for opInd = 1:numel(a.procfn_ops) % count through operations
- inchan = nGetChan + a.procfn_ops{opInd}{4};
- scan.loops(1).procfn(end+1).fn(1) = struct( ...
- 'fn', a.procfn_ops{opInd}{1}, ...
- 'args', {a.procfn_ops{opInd}{2}}, ...
- 'inchan', inchan, ...
- 'outchan', nProcFn + opInd ...
- );
- scan.loops(1).procfn(end).dim = a.procfn_ops{opInd}{3};
- if numel(a.procfn_ops{opInd}) >= 5
- scan.loops(1).procfn(end).identifier = a.procfn_ops{opInd}{5};
- end
- end
-
- % Configure display
- scan.figure = a.fig_id;
- if ~isempty(a.fig_position)
- scan.figpos = a.fig_position;
- end
- scan.disp = [];
- for l = 1:length(a.disp_ops)
- for d = a.disp_dim
- scan.disp(end+1).loop = 1;
- scan.disp(end).channel = nGetChan + a.disp_ops(l);
- scan.disp(end).dim = d;
-
- if a.disp_ops(l) <= nOperations
- opInd = a.disp_ops(l);
- else
- opInd = a.procfn_ops{a.disp_ops(l)-nOperations}{4};
- end
-
- % added new condition "numel(opInd) == 1" to check for several
- % inchans, later they should get a proper title (marcel)
- if numel(opInd) == 1 && opInd <= numel(a.operations)
- scan.disp(end).title = prepare_title(sprintf(['%s: '], a.operations{opInd}{:}));
- elseif numel(opInd) == 1 && length(a.procfn_ops{opInd - nOperations}) > 4
- scan.disp(end).title = prepare_title(sprintf(['%s: '], a.procfn_ops{opInd - nOperations}{5}));
- else
- scan.disp(end).title = '';
- end
- end
- end
-
- if a.saveloop > 0
- scan.saveloop = [1, a.saveloop];
- end
-
-end
-
-
-
-function str = prepare_title(str)
-
- str = strrep(str, '_', ' ');
- str = str(1:end-2);
-
- str = strrep(str, 'RepAverage', 'RSA');
- str = strrep(str, 'Downsample', 'DS');
- str = strrep(str, 'Qubit', 'Q');
-
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/conf_seq_procfn.m b/MATLAB/+qc/conf_seq_procfn.m
deleted file mode 100644
index 8a884b2dd..000000000
--- a/MATLAB/+qc/conf_seq_procfn.m
+++ /dev/null
@@ -1,15 +0,0 @@
-function scan = conf_seq_procfn(scan)
- % Dynamically changes the procfn arguments for each operation which
- % extracts the data from the channel ATSV
- %
- % Assumes that the field scan.data.daq_operations_length has been set to
- % the lengths of the operations.
-
- nGetChan = numel(scan.loops(1).getchan);
- lengths = scan.data.daq_operations_length;
- startInd = 1;
- for p = 1:numel(lengths)
- scan.loops(1).procfn(nGetChan + p).fn(1).args = {startInd, startInd+lengths(p)-1};
- scan.loops(1).procfn(nGetChan + p).dim = [lengths(p)];
- startInd = startInd + lengths(p);
- end
\ No newline at end of file
diff --git a/MATLAB/+qc/daq_operations.m b/MATLAB/+qc/daq_operations.m
deleted file mode 100644
index cb1bc51b5..000000000
--- a/MATLAB/+qc/daq_operations.m
+++ /dev/null
@@ -1,125 +0,0 @@
-function [output, bool, msg] = daq_operations(ctrl, varargin)
-
- global plsdata smdata
- hws = plsdata.awg.hardwareSetup;
- daq = plsdata.daq.inst;
- instIndex = sminstlookup(plsdata.daq.instSmName);
-
- program = struct();
- msg = '';
- bool = false;
-
- default_args = struct(...
- 'program_name', 'default_program', ...
- 'operations', {plsdata.daq.defaultOperations}, ...
- 'verbosity', 10 ...
- );
- a = util.parse_varargin(varargin, default_args);
- output = a.operations;
-
- % --- add ---------------------------------------------------------------
- if strcmp(ctrl, 'add') % output is operations
- % Call before qc.awg_program('arm')!
-
- smdata.inst(instIndex).data.virtual_channel = struct( ...
- 'operations', {a.operations} ...
- );
-
- % alazar.update_settings = py.True is automatically set if
- % register_operations is executed. This results in reconfiguration
- % of the Alazar which takes a long time. Thus, we avoid registering
- % operations if the last armed program is the same as the currently
- % armed program. We know plsdata.awg.currentProgam contains the last
- % armed program since qc.daq_operations should be called before
- % qc.awg_program('arm').
- if plsdata.daq.reuseOperations && ~plsdata.daq.operationsExternallyModified && strcmp(plsdata.awg.currentProgam, a.program_name)
- msg = sprintf('Operations from last armed program ''%s'' reused.\n If an error occurs, try executing another program\n first to update the operations.', plsdata.awg.currentProgam);
- else
- daq.register_operations(a.program_name, qc.operations_to_python(a.operations));
- msg = sprintf('Operations for program ''%s'' added', a.program_name);
-
- if plsdata.daq.operationsExternallyModified
- plsdata.daq.inst.update_settings = py.True;
- end
-
- plsdata.daq.operationsExternallyModified = false;
- % qc.workaround_alazar_single_buffer_acquisition();
- end
- bool = true;
-
- % --- set length --------------------------------------------------------
- elseif strcmp(ctrl, 'set length') % output is length
- % Operations need to have been added beforehand
- output = qc.daq_operations('get length', a);
- smdata.inst(instIndex).cntrlfn([instIndex nan 999], output);
-
- % --- get length --------------------------------------------------------
- elseif strcmp(ctrl, 'get length') % output is length
- % Operations need to have been added beforehand
- mask_maker = py.getattr(daq, '_make_mask');
- masks = util.py.py2mat(py.getattr(daq, '_registered_programs'));
- masks = util.py.py2mat(masks.(a.program_name));
- operations = masks.operations;
- masks = util.py.py2mat(masks.masks(mask_maker));
-
-
- maskIdsFromOperations = cellfun(@(x)(char(x.maskID)), util.py.py2mat(operations), 'UniformOutput', false);
- maskIdsFromMasks = cellfun(@(x)(char(x.identifier)), util.py.py2mat(masks), 'UniformOutput', false);
-
- output = [];
- for k = 1:length(operations)
- maskIndex = find( cellfun(@(x)(strcmp(x, maskIdsFromOperations{k})), maskIdsFromMasks) );
- if numel(maskIndex) ~= 1
- error('Found several masks with same identifier. Might be a problem in qctoolkit or in this function.');
- end
-
- if isa(operations{k}, 'py.atsaverage._atsaverage_release.ComputeDownsampleDefinition')
- output(k) = util.py.py2mat(size(masks{maskIndex}.length));
- elseif isa(operations{k}, 'py.atsaverage._atsaverage_release.ComputeRepAverageDefinition')
- n = util.py.py2mat(masks{maskIndex}.length.to_ndarray);
- if any(n ~= n(1))
- error('daq_operations assumes that all masks should have the same length if using ComputeRepAverageDefinition.');
- end
- output(k) = n(1);
- else
- error('Operation ''%s'' not yet implemented', class(operations{k}));
- end
- end
- if isempty(output)
- warning('No masks configured');
- end
-
- % --- get ---------------------------------------------------------------
- elseif strcmp(ctrl, 'get programs') % output is registered programs
- % Operations need to have been added beforehand
- % masks = util.py.py2mat(daq.config.masks); % this worked sometimes but sometimes not
- output = util.py.py2mat(py.getattr(daq, '_registered_programs'));
-
- % --- remove ------------------------------------------------------------
- elseif strcmp(ctrl, 'remove') % output is operations
- % Should not call this usually. Call qc.awg_program('remove') instead.
- smdata.inst(instIndex).data.virtual_channel = struct( ...
- 'operations', {{}} ...
- );
- programs = fieldnames(qc.daq_operations('get programs'));
- if any(cellfun(@(x)(strcmp(x, a.program_name)), programs))
- daq.delete_program(a.program_name);
- msg = sprintf('Operations for program ''%s'' deleted', a.program_name);
- bool = true;
- else
- msg = sprintf('Operations for program ''%s'' were not registered', a.program_name);
- bool = true;
- end
-
- % --- clear all ---------------------------------------------------------
- elseif strcmp(ctrl, 'clear all')
- alazarPackage = py.importlib.import_module('qctoolkit.hardware.dacs.alazar');
- py.setattr(daq, '_registered_programs', py.collections.defaultdict(alazarPackage.AlazarProgram));
- bool = true;
- msg = 'All programs cleared from DAQ';
-
- end
-
- if a.verbosity > 9
- fprintf([msg '\n']);
- end
\ No newline at end of file
diff --git a/MATLAB/+qc/dict.m b/MATLAB/+qc/dict.m
deleted file mode 100644
index 9e8675ba6..000000000
--- a/MATLAB/+qc/dict.m
+++ /dev/null
@@ -1,10 +0,0 @@
-function d = dict(varargin)
-% Wrapper for struct so do not need to have three curly braces when
-% creating pulse templates
-%
-% varargin needs to the same as for struct(.)
-
-for k = 2:2:numel(varargin)
- varargin{k} = {varargin{k}};
-end
-d = struct(varargin{:});
\ No newline at end of file
diff --git a/MATLAB/+qc/dict_apply_globals.m b/MATLAB/+qc/dict_apply_globals.m
deleted file mode 100644
index 7fe8665bf..000000000
--- a/MATLAB/+qc/dict_apply_globals.m
+++ /dev/null
@@ -1,27 +0,0 @@
-function d = dict_apply_globals(d)
- % Replace all parameters by their global values
- if qc.is_dict(d) && isfield(d, 'global')
- delim = '___';
- globals = fieldnames(d.global);
-
- for pulseName = fieldnames(d)'
- if strcmp(pulseName{1}, strcat('dict', delim, 'name'))
- continue
- end
-
- % Only add global parameters which are defined in pulse parameters
- % for paramName = fieldnames(d.(pulseName{1}))'
- % bool = cellfun(@(x)(strcmp(paramName{1}, x)), globals, 'UniformOutput', true);
- % if any(bool)
- % d.(pulseName{1}).(paramName{1}) = d.global.(globals{bool});
- % end
- % end
-
- % Add all global parameters to pulse irrespective of whether they are
- % defined in pulse parameters
- d.(pulseName{1}) = qc.join_structs(d.(pulseName{1}), d.global);
-
- end
- d = rmfield(d, 'global');
- end
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/dict_to_parameter_struct.m b/MATLAB/+qc/dict_to_parameter_struct.m
deleted file mode 100644
index 2d997c9bc..000000000
--- a/MATLAB/+qc/dict_to_parameter_struct.m
+++ /dev/null
@@ -1,18 +0,0 @@
-function p = dict_to_parameter_struct(d)
- % Flatten dict into a parameter struct
- if qc.is_dict(d)
- delim = '___';
- d = rmfield(d, strcat('dict', delim, 'name'));
-
- p = {};
- for pulseName = fieldnames(d)'
- for paramName = fieldnames(d.(pulseName{1}))'
- p{end+1} = strcat(pulseName{1}, delim, paramName{1});
- p{end+1} = d.(pulseName{1}).(paramName{1});
- end
- end
- p = struct(p{:});
- else
- p = d;
- end
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/disp_awg_seq_table.m b/MATLAB/+qc/disp_awg_seq_table.m
deleted file mode 100644
index 7dc7a2781..000000000
--- a/MATLAB/+qc/disp_awg_seq_table.m
+++ /dev/null
@@ -1,95 +0,0 @@
-% function to display the sequence table hold by qctoolkit Tabor instance
-% or given in the varargins
-% -------------------------------------------------------------------------
-% Notes:
-% - if varargin.seq_table is empty the sequence table saved in the qctoolkit
-% Tabor object is plotted -> function uses qc.get_sequence_table internaly
-% -------------------------------------------------------------------------
-% written by Marcel Meyer 08|2018
-
-
-function disp_awg_seq_table(varargin)
-
- global plsdata
-
- defaultArgs = struct(...
- 'seq_table', {{}}, ...
- 'programName', plsdata.awg.currentProgam, ...
- 'advancedSeqTableFlag', false ...
- );
- args = util.parse_varargin(varargin, defaultArgs);
-
-
- if isempty(args.seq_table)
- seq_table = qc.get_sequence_table(args.programName, args.advancedSeqTableFlag);
- else
- assert(iscell(args.seq_table), 'wrong format sequence table')
- seq_table = args.seq_table;
- end
-
- disp(' ');
- disp('[i] Table 1 is for channel pair AB and table 2 for channel pair CD.');
- disp(' ');
-
- counter = 0;
- tmpEntry = '';
-
- for k = 1:2
- if isempty(seq_table{k})
- warning('-- empty sequence table at channel nr %i -- \n', k);
- else
- if ~args.advancedSeqTableFlag
-
- fprintf('--- table %d -----------------\n', k);
- for n = 1:length(seq_table{k})
- fprintf(' -- sub table %d ---------\n', n);
-
- tmpEntry = seq_table{k}{n}{1};
- counter = 0;
- for i=1:length(seq_table{k}{n})
- if isequal(tmpEntry, seq_table{k}{n}{i})
- counter = counter+1;
- else
-
- fprintf(' rep = %d', counter);
- disp(tmpEntry);
-
- tmpEntry = seq_table{k}{n}{i};
- counter = 1;
- end
- end
- fprintf(' rep = %d', counter);
- disp(tmpEntry);
- disp('-----------------------------')
- end
-
-
-
-
- else
-
- fprintf('--- table %d -----------------\n', k);
-
- tmpEntry = seq_table{k}{1};
- counter = 0;
- for i=1:length(seq_table{k})
- if isequal(tmpEntry, seq_table{k}{i})
- counter = counter+1;
- else
- fprintf(' rep = %d', counter);
- disp(tmpEntry);
-
- tmpEntry = seq_table{k}{i};
- counter = 0;
- end
- end
- fprintf(' rep = %d', counter);
- disp(tmpEntry);
- disp('-----------------------------')
-
- end
-
-
- end
- end
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/disp_dict.m b/MATLAB/+qc/disp_dict.m
deleted file mode 100644
index bdc50ac37..000000000
--- a/MATLAB/+qc/disp_dict.m
+++ /dev/null
@@ -1,31 +0,0 @@
-function text = disp_dict(dict_string_or_struct)
- global plsdata
- delim = '___';
-
- text = '';
-
- if isstruct(dict_string_or_struct)
- if ~isfield(dict_string_or_struct, ['dict' delim 'name'])
- error('Please pass a valid dictionary struct. The passed argument is missing the field ''%s''.\n', ['dict' delim 'name']);
- end
- text = py.json.dumps(dict_string_or_struct, pyargs('indent', int8(4), 'sort_keys', true));
- text = char(text);
- dict_string_or_struct = dict_string_or_struct.(['dict' delim 'name']);
- elseif ischar(dict_string_or_struct)
- file_name = fullfile(plsdata.dict.path, [dict_string_or_struct '.json']);
- if exist(file_name, 'file')
- text = fileread(file_name);
- else
- error('Dictionary ''%s'' could not be loaded since file ''%s'' does not exist\n', dict_string_or_struct, file_name);
- end
- else
- error('Please pass a valid dictonary struct or string\n');
- end
-
- if ~isempty(text)
- util.disp_section(sprintf('Dictionary %s', dict_string_or_struct));
- fprintf('%s\n', text);
- util.disp_section();
- end
-
- end
\ No newline at end of file
diff --git a/MATLAB/+qc/get_alazar_measurements.m b/MATLAB/+qc/get_alazar_measurements.m
deleted file mode 100644
index a5d79e86c..000000000
--- a/MATLAB/+qc/get_alazar_measurements.m
+++ /dev/null
@@ -1,32 +0,0 @@
-function [mask_prototypes, measurement_map, txt] = get_alazar_measurements(varargin)
-
- global plsdata
- hws = plsdata.awg.hardwareSetup;
- daq = plsdata.daq.inst;
-
-
- defaultArgs = struct( ...
- 'disp', true ...
- );
- args = util.parse_varargin(varargin, defaultArgs);
-
- mask_prototypes = util.py.py2mat(daq.mask_prototypes);
- measurement_map = util.py.py2mat(py.getattr(hws,'_measurement_map'));
-
- txt = sprintf('%-30s %-30s %-30s\\n', 'Measurement', 'Mask', 'Hardware Channel');
- txt = strcat(txt, [ones(1,85)*'-' '\n']);
-
- measurement_map = orderfields(measurement_map);
- for measName = fieldnames(measurement_map)'
- masks = measurement_map.(measName{1});
- for k = 1:numel(masks)
- maskName = char(masks{k}.mask_name);
- txt = strcat(txt, sprintf('%-30s %-30s %-30i\\n', measName{1}, maskName, mask_prototypes.(maskName){1}));
- end
- end
-
- txt = strcat(txt, [ones(1,85)*'-' '\n']);
-
- if args.disp
- fprintf(txt);
- end
\ No newline at end of file
diff --git a/MATLAB/+qc/get_awg_channels.m b/MATLAB/+qc/get_awg_channels.m
deleted file mode 100644
index 067b7dd4a..000000000
--- a/MATLAB/+qc/get_awg_channels.m
+++ /dev/null
@@ -1,16 +0,0 @@
-function [analogNames, markerNames, channels] = get_awg_channels()
-
- global plsdata
-
- % Get AWG analog channels and markers
- channels = struct(plsdata.awg.hardwareSetup.registered_channels);
- analogNames = {};
- markerNames = {};
- for chanName = fieldnames(channels)'
- chan = util.py.py2mat(channels.(chanName{1}));
- if isa(chan{1}, 'py.qctoolkit.hardware.setup.MarkerChannel')
- markerNames{end+1} = chanName{1};
- elseif isa(chan{1}, 'py.qctoolkit.hardware.setup.PlaybackChannel')
- analogNames{end+1} = chanName{1};
- end
- end
\ No newline at end of file
diff --git a/MATLAB/+qc/get_awg_memory.m b/MATLAB/+qc/get_awg_memory.m
deleted file mode 100644
index 7b830b479..000000000
--- a/MATLAB/+qc/get_awg_memory.m
+++ /dev/null
@@ -1,44 +0,0 @@
-% function to get waveforms and sequence tables from the AWG
-% -------------------------------------------------------------------------
-% Notes:
-% - the function only works with the Tabor AWG Simulator not on the real
-% Tabor AWG
-% - the function arms the program that is inspected
-% -------------------------------------------------------------------------
-% written by Marcel Meyer 08|2018
-
-function awg_memory_struct = get_awg_memory(program_name, awg_channel_pair_identifier)
-
- global plsdata
-
- assert(ischar(program_name), 'first argument of get_awg_memory must be string');
-
- if nargin < 2 || isempty(awg_channel_pair_identifier)
- awg_channel_pair_identifier = 'AB';
- else
- assert(ischar(awg_channel_pair_identifier), 'second argument of get_awg_memory must be string');
- end
-
- % get AWG channelpair python object
- hws = plsdata.awg.hardwareSetup;
- known_awgs = util.py.py2mat(hws.known_awgs);
- sort_indices = cellfun(@(x)(~isempty(strfind(char(x.identifier), awg_channel_pair_identifier))), known_awgs);
- channelpair = known_awgs(find(sort_indices));
- channelpair = channelpair{1};
-
- % arm program at AWG
- try
- channelpair.arm(program_name);
- catch err
- warning('program seems not to be on AWG, upload it first, returning without returning memory');
- warning(err.message);
- return
- end
-
- % get a plottable program object -> qctoolkit Tabor driver gets sequence
- % tables and waveforms from the simulator
- plottableProgram = channelpair.read_complete_program();
-
- awg_memory_struct = util.py.py2mat(plottableProgram.to_builtin());
-
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/get_awg_programs.m b/MATLAB/+qc/get_awg_programs.m
deleted file mode 100644
index 3d764b8e4..000000000
--- a/MATLAB/+qc/get_awg_programs.m
+++ /dev/null
@@ -1,10 +0,0 @@
-function [programNames, programs] = get_awg_programs()
-
- global plsdata
-
- programs = util.py.py2mat(plsdata.awg.hardwareSetup.registered_programs);
- programNames = fieldnames(programs);
-
- if ~isempty(setdiff(fieldnames(rmfield(plsdata.awg.registeredPrograms, 'currentProgam')), programNames))
- warning('''plsdata.awg.registeredPrograms'' out of sync with ''plsdata.awg.hardwareSetup.registered_programs''. Clear all programs by executing qc.awg_program(''clear all'') to remedy.');
- end
\ No newline at end of file
diff --git a/MATLAB/+qc/get_awg_seq_table.m b/MATLAB/+qc/get_awg_seq_table.m
deleted file mode 100644
index 36fb340fc..000000000
--- a/MATLAB/+qc/get_awg_seq_table.m
+++ /dev/null
@@ -1,13 +0,0 @@
-function seq_table = get_awg_seq_table(varargin)
-
- global plsdata
-
- defaultArgs = struct(...
- 'programName', plsdata.awg.currentProgam, ...
- 'advancedSeqTableFlag', false ...
- );
- args = util.parse_varargin(varargin, defaultArgs);
-
- seq_table = qc.get_sequence_table(args.programName, args.advancedSeqTableFlag);
-
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/get_dnp_pulse_duration.m b/MATLAB/+qc/get_dnp_pulse_duration.m
deleted file mode 100644
index 5e2188ea8..000000000
--- a/MATLAB/+qc/get_dnp_pulse_duration.m
+++ /dev/null
@@ -1,55 +0,0 @@
-function currentPulseDurationOffset = get_dnp_pulse_duration_offset()
-
- global tunedata
-
- t = tunedata.run{tunedata.runIndex}.global_opts.dnp.durations;
-
-% qc.get_pulse_duration(...
-% qc.struct_to_pulse(tunedata.run{tunedata.runIndex}.global_opts.dnp.scan.configfn(4).args{end}.pulse_template), ...
-% qc.join_params_and_dicts('common', 'common_d12', 'common_d34', 'common_d12 d12', 'common_d34 d34')....
-% );
-
-% t_pumpingPart = qc.get_pulse_duration(...
-% qc.load_pulse('dnp_ABCD_4chan'), ...
-% qc.join_params_and_dicts('common', 'common_d12', 'common_d34', 'common_d12 d12', 'common_d34 d34')....
-% );
-%
-% t_s_AB = qc.get_pulse_duration(...
-% qc.load_pulse('s_pumping_AB_4chan'), ...
-% qc.join_params_and_dicts('common', 'common_d12', 'common_d34', 'common_d12 d12', 'common_d34 d34')....
-% );
-% t_cs_AB = qc.get_pulse_duration(...
-% qc.load_pulse('t_pumping_AB_4chan'), ...
-% qc.join_params_and_dicts('common', 'common_d12', 'common_d34', 'common_d12 d12', 'common_d34 d34')....
-% );
-% t_t_AB = qc.get_pulse_duration(...
-% qc.load_pulse('cs_pumping_AB_4chan'), ...
-% qc.join_params_and_dicts('common', 'common_d12', 'common_d34', 'common_d12 d12', 'common_d34 d34')....
-% );
-%
-% t_s_CD = qc.get_pulse_duration(...
-% qc.load_pulse('s_pumping_CD_4chan'), ...
-% qc.join_params_and_dicts('common', 'common_d12', 'common_d34', 'common_d12 d12', 'common_d34 d34')....
-% );
-% t_t_CD = qc.get_pulse_duration(...
-% qc.load_pulse('t_pumping_CD_4chan'), ...
-% qc.join_params_and_dicts('common', 'common_d12', 'common_d34', 'common_d12 d12', 'common_d34 d34')....
-% );
-% t_cs_CD = qc.get_pulse_duration(...
-% qc.load_pulse('cs_pumping_CD_4chan'), ...
-% qc.join_params_and_dicts('common', 'common_d12', 'common_d34', 'common_d12 d12', 'common_d34 d34')....
-% );
-
- pumpConf = tunedata.run{1}.global_opts.dnp.pumpingConfigArgs.currentConfig;
-
- t.current_dnp_pulse = ...
- t.s_AB * double(pumpConf.n_s_AB) + ...
- t.t_AB * double(pumpConf.n_t_AB) + ...
- t.cs_AB * double(pumpConf.n_cs_AB) + ...
- t.s_CD * double(pumpConf.n_s_CD) + ...
- t.t_CD * double(pumpConf.n_t_CD) + ...
- t.cs_CD * double(pumpConf.n_cs_CD);
-
- currentPulseDurationOffset = t.current_dnp_pulse - t.dict_dnp_pulse;
-
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/get_dnp_pulse_duration_offset.m b/MATLAB/+qc/get_dnp_pulse_duration_offset.m
deleted file mode 100644
index 5e2188ea8..000000000
--- a/MATLAB/+qc/get_dnp_pulse_duration_offset.m
+++ /dev/null
@@ -1,55 +0,0 @@
-function currentPulseDurationOffset = get_dnp_pulse_duration_offset()
-
- global tunedata
-
- t = tunedata.run{tunedata.runIndex}.global_opts.dnp.durations;
-
-% qc.get_pulse_duration(...
-% qc.struct_to_pulse(tunedata.run{tunedata.runIndex}.global_opts.dnp.scan.configfn(4).args{end}.pulse_template), ...
-% qc.join_params_and_dicts('common', 'common_d12', 'common_d34', 'common_d12 d12', 'common_d34 d34')....
-% );
-
-% t_pumpingPart = qc.get_pulse_duration(...
-% qc.load_pulse('dnp_ABCD_4chan'), ...
-% qc.join_params_and_dicts('common', 'common_d12', 'common_d34', 'common_d12 d12', 'common_d34 d34')....
-% );
-%
-% t_s_AB = qc.get_pulse_duration(...
-% qc.load_pulse('s_pumping_AB_4chan'), ...
-% qc.join_params_and_dicts('common', 'common_d12', 'common_d34', 'common_d12 d12', 'common_d34 d34')....
-% );
-% t_cs_AB = qc.get_pulse_duration(...
-% qc.load_pulse('t_pumping_AB_4chan'), ...
-% qc.join_params_and_dicts('common', 'common_d12', 'common_d34', 'common_d12 d12', 'common_d34 d34')....
-% );
-% t_t_AB = qc.get_pulse_duration(...
-% qc.load_pulse('cs_pumping_AB_4chan'), ...
-% qc.join_params_and_dicts('common', 'common_d12', 'common_d34', 'common_d12 d12', 'common_d34 d34')....
-% );
-%
-% t_s_CD = qc.get_pulse_duration(...
-% qc.load_pulse('s_pumping_CD_4chan'), ...
-% qc.join_params_and_dicts('common', 'common_d12', 'common_d34', 'common_d12 d12', 'common_d34 d34')....
-% );
-% t_t_CD = qc.get_pulse_duration(...
-% qc.load_pulse('t_pumping_CD_4chan'), ...
-% qc.join_params_and_dicts('common', 'common_d12', 'common_d34', 'common_d12 d12', 'common_d34 d34')....
-% );
-% t_cs_CD = qc.get_pulse_duration(...
-% qc.load_pulse('cs_pumping_CD_4chan'), ...
-% qc.join_params_and_dicts('common', 'common_d12', 'common_d34', 'common_d12 d12', 'common_d34 d34')....
-% );
-
- pumpConf = tunedata.run{1}.global_opts.dnp.pumpingConfigArgs.currentConfig;
-
- t.current_dnp_pulse = ...
- t.s_AB * double(pumpConf.n_s_AB) + ...
- t.t_AB * double(pumpConf.n_t_AB) + ...
- t.cs_AB * double(pumpConf.n_cs_AB) + ...
- t.s_CD * double(pumpConf.n_s_CD) + ...
- t.t_CD * double(pumpConf.n_t_CD) + ...
- t.cs_CD * double(pumpConf.n_cs_CD);
-
- currentPulseDurationOffset = t.current_dnp_pulse - t.dict_dnp_pulse;
-
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/get_minimal_program.m b/MATLAB/+qc/get_minimal_program.m
deleted file mode 100644
index 43c6c8241..000000000
--- a/MATLAB/+qc/get_minimal_program.m
+++ /dev/null
@@ -1,24 +0,0 @@
-function reduced_program = get_minimal_program(program)
- % Return program with all parameters not needed by its pulse template
- % removed
-
- pulse_template = qc.struct_to_pulse(program.pulse_template);
- parameter_names = util.py.py2mat(pulse_template.parameter_names);
-
- program.parameters_and_dicts = qc.join_params_and_dicts(program.parameters_and_dicts);
- reduced_program = program;
-
- % amplitude_at_upload is only used to keep track of the AWG amplitude if
- % it was dynamically changed.
- if isfield(reduced_program, 'amplitudes_at_upload')
- reduced_program = rmfield(reduced_program, 'amplitudes_at_upload');
- end
-
- reduced_program.parameters_and_dicts = struct();
- % Remove all fields in parameters_and_dicts not needed by pulse_template
- for p = parameter_names
- reduced_program.parameters_and_dicts.(p{1}) = program.parameters_and_dicts.(p{1});
- end
-
-
-
\ No newline at end of file
diff --git a/MATLAB/+qc/get_optimal_awg_time.m b/MATLAB/+qc/get_optimal_awg_time.m
deleted file mode 100644
index 858a277d5..000000000
--- a/MATLAB/+qc/get_optimal_awg_time.m
+++ /dev/null
@@ -1,24 +0,0 @@
-function [optimalTime, addTime, optimalNsamp] = get_optimal_awg_time(desiredTime, sampleRate)
- % Tabor AWG closest optimal number of samples: sampleQuantum + n*sampleQuantum
- % Time is in s, sampleRate is in Sa/s
-
- global plsdata
-
- sampleQuantum = plsdata.awg.sampleQuantum;
- minSamples = plsdata.awg.minSamples;
-
- global plsdata
- if nargin < 2 || isempty(sampleRate)
- sampleRate = plsdata.awg.sampleRate;
- end
-
- desiredNsamp = desiredTime*sampleRate;
-
- if desiredNsamp <= minSamples
- optimalNsamp = minSamples;
- else
- optimalNsamp = minSamples + sampleQuantum*round((desiredNsamp-minSamples)/sampleQuantum);
- end
-
- optimalTime = optimalNsamp / sampleRate;
- addTime = optimalTime - desiredTime;
\ No newline at end of file
diff --git a/MATLAB/+qc/get_optimal_pulse_time.m b/MATLAB/+qc/get_optimal_pulse_time.m
deleted file mode 100644
index f11d48c4b..000000000
--- a/MATLAB/+qc/get_optimal_pulse_time.m
+++ /dev/null
@@ -1,10 +0,0 @@
-function [optimalTime, addTime, optimalNsamp, actualTime] = get_optimal_pulse_time(pulse, varargin)
-
- if ischar(pulse)
- pulse = qc.load_pulse(pulse);
- end
-
- actualTime = qc.get_pulse_duration(pulse, qc.join_params_and_dicts(varargin{:}));
- [optimalTime, addTime, optimalNsamp] = qc.get_optimal_awg_time(actualTime);
-
-
\ No newline at end of file
diff --git a/MATLAB/+qc/get_program.m b/MATLAB/+qc/get_program.m
deleted file mode 100644
index f5d0a79c8..000000000
--- a/MATLAB/+qc/get_program.m
+++ /dev/null
@@ -1,12 +0,0 @@
-function [program, channels] = get_program(pulse, varargin)
-
- if ischar(pulse)
- pulse = qc.load_pulse(pulse);
- end
-
- instantiated_pulse = qc.instantiate_pulse(pulse, 'parameters', qc.join_params_and_dicts(varargin{:}));
-
- tmp = py.qctoolkit.hardware.program.MultiChannelProgram(instantiated_pulse);
- program = py.next(py.iter(tmp.programs.values()));
- channels = py.next(py.iter(tmp.programs.keys()));
-
\ No newline at end of file
diff --git a/MATLAB/+qc/get_pulse_duration.m b/MATLAB/+qc/get_pulse_duration.m
deleted file mode 100644
index 50aaaa154..000000000
--- a/MATLAB/+qc/get_pulse_duration.m
+++ /dev/null
@@ -1,15 +0,0 @@
-function pulse_length = get_pulse_duration(pulse_template, parameters)
- % Return pulse length in s
-
- kwargs = cell2namevalpairs(fieldnames(parameters), struct2cell(parameters));
- pulse_length = py.getattr(pulse_template, 'duration').evaluate_numeric(pyargs(kwargs{:}))*1e-9;
-
-
-function cellarr = cell2namevalpairs(field_names, values)
- cellarr={};
- for k = 1:numel(field_names)
- cellarr{end+1}=field_names{k};
- cellarr{end+1}=values{k};
- end
-
-
\ No newline at end of file
diff --git a/MATLAB/+qc/get_pulse_params.m b/MATLAB/+qc/get_pulse_params.m
deleted file mode 100644
index 210a67ada..000000000
--- a/MATLAB/+qc/get_pulse_params.m
+++ /dev/null
@@ -1,9 +0,0 @@
-function pulseParameters = get_pulse_params(pulse_name_or_template)
-
- if ischar(pulse_name_or_template)
- pulse_template = qc.load_pulse(pulse_name_or_template);
- else
- pulse_template = pulse_name_or_template;
- end
- pulseParameters = sort(util.py.py2mat(pulse_template.parameter_names));
-
\ No newline at end of file
diff --git a/MATLAB/+qc/get_pumping_from_awg.m b/MATLAB/+qc/get_pumping_from_awg.m
deleted file mode 100644
index 605c7b81b..000000000
--- a/MATLAB/+qc/get_pumping_from_awg.m
+++ /dev/null
@@ -1,68 +0,0 @@
-function pumpingConfig = get_pumping_from_awg(varargin)
-
-global plsdata
-
-defaultArgs = struct(...
- 'programName', plsdata.awg.currentProgam ...
- );
-args = util.parse_varargin(varargin, defaultArgs);
-
-pumpingConfig = struct();
-
-seqTable = qc.get_sequence_table(args.programName, false);
-seqTableCheck = true;
-report = '';
-pumpSubTab = seqTable{1}{end};
-
-
-
-
-
-%---------------- some checks ---------------------------------------------
-
-%check if there are six entries for the three pumping types for each qubit
-if length(pumpSubTab) < 6
- seqTableCheck = false;
- report = ' -- There are not six waveforms at the end of the sequence table! They might be put together or not uploaded or not at the end of the sequence table. -- ';
-end
-
-%test if every waveform is different/has a different
-if seqTableCheck
- for i = 0:5
- for j = 0:5
- if (i~=j) && (pumpSubTab{end-i}{2} == pumpSubTab{end-j}{2})
- report = ' -- Not all waveforms for pumping (that are assumed to be different) are different to each other! -- ';
- seqTableCheck = false;
- end
- end
- end
-end
-
-%test if both channel pairs have the same pumping sequence table part
-for i = 1:6
- if seqTableCheck && ~isequal(seqTable{1}{end}{end-i+1}, seqTable{2}{end}{end-i+1})
- report = ' -- Not the same pumping configuration on both channel pairs of the AWG! -- ';
- seqTableCheck = false;
- end
-end
-
-
-
-
-%------------- reading out the pumping configuration ----------------------
-
-if ~seqTableCheck
- warning(report);
-else
- pumpingConfig.n_s_AB = pumpSubTab{end-5}{1};
- pumpingConfig.n_t_AB = pumpSubTab{end-4}{1};
- pumpingConfig.n_cs_AB = pumpSubTab{end-3}{1};
- pumpingConfig.n_s_CD = pumpSubTab{end-2}{1};
- pumpingConfig.n_t_CD = pumpSubTab{end-1}{1};
- pumpingConfig.n_cs_CD = pumpSubTab{end-0}{1};
-end
-
-
-
-
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/get_segment_waveform.m b/MATLAB/+qc/get_segment_waveform.m
deleted file mode 100644
index 8526e4906..000000000
--- a/MATLAB/+qc/get_segment_waveform.m
+++ /dev/null
@@ -1,53 +0,0 @@
-function [wf1, wf2] = get_segment_waveform(program_name, channel_pair_index, memory_index, awg_channel_pair_identifiers)
-% Get Wafeform of Sequencer Table Element
-% PLEASE NOTE: works only for the Tabor AWG SIMULATOR
-% PLEASE NOTE: program gets armed by calling this function
-%
-% --- Outputs -------------------------------------------------------------
-% wf1 : first channel y-values of AWG channelpair
-% wf2 : second channel y-values of AWG channelpair
-%
-% --- Inputs --------------------------------------------------------------
-% program_name : Program name for which wafeform is
-% returned
-% channel_pair_index : 1 for channelpair AB and 2 for channelpair
-% CD. Also see awg_channel_pair_identifier
-% input
-% memory_index : identifier number of element at the Tabor
-% AWG (corresponds to second column in
-% Sequencer Table
-% awg_channel_pair_identifiers : Some substring in the channel pair
-% identifiers to be matched. Sequence tables
-% are sorted in the same order as channel
-% pair identifiers substrings passed in this
-% variable. Default is {'AB', 'CD'}.
-%
-% -------------------------------------------------------------------------
-% 2018/08 Marcel Meyer
-% (marcel.meyer1@rwth-aachen.de)
-
-global plsdata
- hws = plsdata.awg.hardwareSetup;
-
- if nargin < 4 || isempty(awg_channel_pair_identifiers)
- awg_channel_pair_identifiers = {'AB', 'CD'};
- end
-
- known_awgs = util.py.py2mat(hws.known_awgs);
- sort_indices = cellfun(@(x)(find( cellfun(@(y)(~isempty(strfind(char(x.identifier), y))), awg_channel_pair_identifiers) )), known_awgs);
- known_awgs = known_awgs(sort_indices);
-
- %one has to arm the program to access the plottableProgram object of the
- %program
- known_awgs{channel_pair_index}.arm(program_name);
-
- plottableProgram = known_awgs{channel_pair_index}.read_complete_program();
-
- wf1 = plottableProgram.get_segment_waveform(uint8(0), uint8(memory_index));
- wf2 = plottableProgram.get_segment_waveform(uint8(1), uint8(memory_index));
-
- wf1 = util.py.py2mat(wf1);
- wf2 = util.py.py2mat(wf2);
-
- wf1 = cell2mat(wf1);
- wf2 = cell2mat(wf2);
\ No newline at end of file
diff --git a/MATLAB/+qc/get_sequence_table.m b/MATLAB/+qc/get_sequence_table.m
deleted file mode 100644
index 489c6e835..000000000
--- a/MATLAB/+qc/get_sequence_table.m
+++ /dev/null
@@ -1,91 +0,0 @@
-function seq_table = get_sequence_table(program_name, advanced_seq_table_flag, awg_channel_pair_identifiers, verbosity, return_python_list)
-% GET_SEQUENCE_TABLE Get sequence table of program on Tabor AWG
-% (not actually from AWG but from the qctoolkit Tabor Driver instance)
-%
-% --- Outputs -------------------------------------------------------------
-% seq_table : Cell of sequence tables for each Tabor
-% channel pair
-%
-% --- Inputs --------------------------------------------------------------
-% program_name : Program name for which sequence table is
-% returned
-% advanced_seq_table_flag : Get advanced sequence table if true.
-% Default is false.
-% awg_channel_pair_identifiers : Some substring in the channel pair
-% identifiers to be matched. Sequence tables
-% are sorted in the same order as channel
-% pair identifiers substrings passed in this
-% variable. Default is {'AB', 'CD'}.
-% verbosity : Print sequence table to command line.
-% Default is 0.
-% return_python_list : Returns a python list object instead of a
-% matlab cell. This makes the function
-% faster as the conversion is slow.
-% Dafault is false.
-%
-% -------------------------------------------------------------------------
-% (c) 2018/06 Pascal Cerfontaine and Marcel Meyer
-% (cerfontaine@physik.rwth-aachen.de)
-
-global plsdata
-hws = plsdata.awg.hardwareSetup;
-
-if nargin < 2 || isempty(advanced_seq_table_flag)
- advanced_seq_table_flag = false;
-end
-if nargin < 3 || isempty(awg_channel_pair_identifiers)
- awg_channel_pair_identifiers = {'AB', 'CD'};
-end
-if nargin < 4 || isempty(verbosity)
- verbosity = 0;
-end
-if nargin < 5 || isempty(return_python_list)
- return_python_list = false;
-end
-if advanced_seq_table_flag
- seq_txt = 'A';
-else
- seq_txt = '';
-end
-
-known_awgs = util.py.py2mat(hws.known_awgs);
-sort_indices = cellfun(@(x)(find( cellfun(@(y)(~isempty(strfind(char(x.identifier), y))), awg_channel_pair_identifiers) )), known_awgs);
-known_awgs = known_awgs(sort_indices);
-
-for k = 1:length(known_awgs)
- known_programs{k} = util.py.py2mat(py.getattr(known_awgs{k}, '_known_programs'));
-
- if verbosity > 0
- util.disp_section(sprintf('%s %sST: %s', awg_channel_pair_identifiers{k}, seq_txt, program_name));
- end
-
- if isfield(known_programs{k}, program_name)
- tabor_program{k} = known_programs{k}.(program_name){2};
-
- if advanced_seq_table_flag
- seq_table{k} = py.getattr(tabor_program{k}, '_advanced_sequencer_table');
- else
- seq_table{k} = py.getattr(tabor_program{k}, '_sequencer_tables');
- end
-
- if verbosity > 0
- disp(seq_table{k});
- end
-
- if ~return_python_list
- seq_table{k} = util.py.py2mat(seq_table{k});
- end
- else
- tabor_program{k} = {};
- seq_table{k} = {};
-
- if verbosity > 0
- disp(' Program not present');
- end
- end
-end
-
-if verbosity > 0
- fprintf('\n');
- util.disp_section();
-end
diff --git a/MATLAB/+qc/get_sequence_table_from_simulator.m b/MATLAB/+qc/get_sequence_table_from_simulator.m
deleted file mode 100644
index 776584024..000000000
--- a/MATLAB/+qc/get_sequence_table_from_simulator.m
+++ /dev/null
@@ -1,99 +0,0 @@
-function seq_table = get_sequence_table_from_simulator(program_name, advanced_seq_table_flag, awg_channel_pair_identifiers, verbosity, return_python_list)
-% GET_SEQUENCE_TABLE Get sequence table of program on Tabor AWG Simulator
-% PLEASE NOTE: the program gets armed by the function
-%
-% --- Outputs -------------------------------------------------------------
-% seq_table : Cell of sequence tables for each Tabor
-% channel pair
-%
-% --- Inputs --------------------------------------------------------------
-% program_name : Program name for which sequence table is
-% returned
-% advanced_seq_table_flag : Get advanced sequence table if true.
-% Default is false.
-% awg_channel_pair_identifiers : Some substring in the channel pair
-% identifiers to be matched. Sequence tables
-% are sorted in the same order as channel
-% pair identifiers substrings passed in this
-% variable. Default is {'AB', 'CD'}.
-% verbosity : Print sequence table to command line.
-% Default is 0.
-% return_python_list : Returns a python list object instead of a
-% matlab cell. This makes the function
-% faster as the conversion is slow.
-% Dafault is false.
-%
-% -------------------------------------------------------------------------
-% 2018/08 Marcel Meyer
-% based on qc.get_sequence_table by Pascal Cerfontaine and Marcel Meyer
-% (marcel.meyer1@rwth-aachen.de)
-
- global plsdata
- hws = plsdata.awg.hardwareSetup;
-
- if nargin < 2 || isempty(advanced_seq_table_flag)
- advanced_seq_table_flag = false;
- end
- if nargin < 3 || isempty(awg_channel_pair_identifiers)
- awg_channel_pair_identifiers = {'AB', 'CD'};
- end
- if nargin < 4 || isempty(verbosity)
- verbosity = 0;
- end
-
- if nargin < 5 || isempty(return_python_list)
- return_python_list = false;
- end
-
- if advanced_seq_table_flag
- seq_txt = 'A';
- else
- seq_txt = '';
- end
-
- known_awgs = util.py.py2mat(hws.known_awgs);
- sort_indices = cellfun(@(x)(find( cellfun(@(y)(~isempty(strfind(char(x.identifier), y))), awg_channel_pair_identifiers) )), known_awgs);
- known_awgs = known_awgs(sort_indices);
-
- for k = 1:length(known_awgs)
- known_programs{k} = util.py.py2mat(py.getattr(known_awgs{k}, '_known_programs'));
-
- if verbosity > 0
- util.disp_section(sprintf('%s %sST: %s', awg_channel_pair_identifiers{k}, seq_txt, program_name));
- end
-
- if isfield(known_programs{k}, program_name)
-
- % one has to arm the program before accessing its plottableProgram
- % object
- known_awgs{k}.arm(program_name);
- plottableProgram = known_awgs{k}.read_complete_program();
-
- if advanced_seq_table_flag
- seq_table{k} = py.getattr(plottableProgram, '_advanced_sequence_table');
- else
- seq_table{k} = py.getattr(plottableProgram, '_sequence_tables');
- end
-
- if verbosity > 0
- disp(seq_table{k});
- end
-
- if ~return_python_list
- seq_table{k} = util.py.py2mat(seq_table{k});
- end
-
- else
- tabor_program{k} = {};
- seq_table{k} = {};
-
- if verbosity > 0
- disp(' Program not present');
- end
- end
- end
-
-if verbosity > 0
- fprintf('\n');
- util.disp_section();
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/instantiate_pulse.m b/MATLAB/+qc/instantiate_pulse.m
deleted file mode 100644
index 9e91c397c..000000000
--- a/MATLAB/+qc/instantiate_pulse.m
+++ /dev/null
@@ -1,44 +0,0 @@
-function instantiated_pulse = instantiate_pulse(pulse, varargin)
- % Plug in parameters
-
- if qc.is_instantiated_pulse(pulse)
- instantiated_pulse = pulse;
-
- else
- default_args = struct(...
- 'parameters', py.None, ...
- 'channel_mapping', py.None, ...
- 'window_mapping' , py.None, ...
- 'global_transformation', [], ...
- 'to_single_waveform', py.set() ...
- );
-
- args = util.parse_varargin(varargin, default_args);
-
- args.channel_mapping = replace_empty_with_pynone(args.channel_mapping);
- args.window_mapping = replace_empty_with_pynone(args.window_mapping);
- args.global_transformation = qc.to_transformation(args.global_transformation);
-
- kwargs = pyargs( ...
- 'parameters' , args.parameters, ...
- 'channel_mapping', args.channel_mapping, ...
- 'measurement_mapping' , args.window_mapping, ...
- 'global_transformation', args.global_transformation, ...
- 'to_single_waveform', args.to_single_waveform ...
- );
-
- instantiated_pulse = util.py.call_with_interrupt_check(py.getattr(pulse, 'create_program'), kwargs);
- end
-end
-
-
-function mappingStruct = replace_empty_with_pynone(mappingStruct)
-
- for fn = fieldnames(mappingStruct)'
- if isempty(mappingStruct.(fn{1}))
- mappingStruct.(fn{1}) = py.None;
- end
- end
-
-end
-
diff --git a/MATLAB/+qc/is_dict.m b/MATLAB/+qc/is_dict.m
deleted file mode 100644
index 4e37478c7..000000000
--- a/MATLAB/+qc/is_dict.m
+++ /dev/null
@@ -1,4 +0,0 @@
-function bool = is_dict(dp)
- delim = '___';
- bool = ischar(dp) || (isstruct(dp) && isfield(dp, strcat('dict', delim, 'name')));
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/is_instantiated_pulse.m b/MATLAB/+qc/is_instantiated_pulse.m
deleted file mode 100644
index 6d4d394e3..000000000
--- a/MATLAB/+qc/is_instantiated_pulse.m
+++ /dev/null
@@ -1,3 +0,0 @@
-function bool = is_instantiated_pulse(pulse)
- bool = strcmp(class(pulse), 'py.qctoolkit._program.instructions.ImmutableInstructionBlock');
- bool = bool || strcmp(class(pulse), 'py.qctoolkit._program._loop.Loop');
diff --git a/MATLAB/+qc/join_params_and_dicts.m b/MATLAB/+qc/join_params_and_dicts.m
deleted file mode 100644
index a80f58d6e..000000000
--- a/MATLAB/+qc/join_params_and_dicts.m
+++ /dev/null
@@ -1,62 +0,0 @@
-function parameters = join_params_and_dicts(varargin)
- % Each argument is either a dictionary name as a string, a dictionary
- % struct or a parameter struct. This function joins all input arguments to
- % a single parameters struct. If conflicts occur, the field of the rightmost
- % argument takes precedence.
- %
- % Dictionaries are saved as json files in the repository qctoolkit-dicts.
- % Each dictionary is a struct with a field for each pulse. The field names
- % are the same as the pulse names (as pulse names are unique identifiers).
- % Each field has subfields which store pulse parameters for that pulse.
- %
- % When defining pulses in qctoolkit, prefix each parameter name with the
- % pulsename followed by three underscores. I.e. for a pules 'meas' the
- % parameter 'waiting_time' should be called 'meas___waiting_time'. The
- % dictionary however should only have the parameter field 'waiting_time',
- % i.e struct('meas', struct('waiting_time', 1)).
- %
- % Each dictionary also has a global field which also contains a struct. The
- % field names of this struct refer to parameters which can be set to the
- % same value across different pulses in the same dictionary. For example
- % struct('global', struct('waiting_time', 1)) will set the parameter
- % 'waiting_time' to 1 for all pulses which have the parameter
- % 'waiting_time' in the dictionary where the global is defined.
- %
- % Globals do not take precedence over parameters passed in more to the
- % right.
- %
- % Each dictionary also has a field 'dict___name' which specifies
- % the dictionary name
-
- if numel(varargin) == 1 && iscell(varargin{1})
- p = varargin{1};
- else
- p = varargin;
- end
- p = cellfun(@qc.load_dict, p, 'UniformOutput', false);
- p = cellfun(@qc.dict_apply_globals, p, 'UniformOutput', false);
- p = cellfun(@qc.dict_to_parameter_struct, p, 'UniformOutput', false);
-
- parameters = struct();
- for k = 1:numel(p)
- parameters = join(parameters, p{k});
- end
-
- parameters = qc.array2row(parameters);
- parameters = orderfields(parameters);
-
-end
-
-
-function p = join(p1, p2)
- % Join two parameter structs. This process is additive, fields in p2 take
- % precedence
- p = p1;
-
- for paramName = fieldnames(p2)'
- p.(paramName{1}) = p2.(paramName{1});
- end
-end
-
-
-
diff --git a/MATLAB/+qc/join_structs.m b/MATLAB/+qc/join_structs.m
deleted file mode 100644
index fd8e8ab2d..000000000
--- a/MATLAB/+qc/join_structs.m
+++ /dev/null
@@ -1,17 +0,0 @@
-function S = join_structs(varargin)
- % Fields of arguments passed in more on the left get overwritten by
- % arguments passed in more to the right
-
- S = struct();
- for v = varargin
- S = join_2_structs(S, v{1});
- end
-
-end
-
-function A = join_2_structs(A, B)
- % Fields in B are added to A, fields in A with same name get overwritten
- for f = fieldnames(B).'
- A.(f{1}) = B.(f{1});
- end
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/load_dict.m b/MATLAB/+qc/load_dict.m
deleted file mode 100644
index 100bcf164..000000000
--- a/MATLAB/+qc/load_dict.m
+++ /dev/null
@@ -1,56 +0,0 @@
-function dict_string_or_struct = load_dict(dict_string_or_struct, create_dict)
- % Load dict if d is a string. Otherwise leave d untouched.
- %
- % Important: this does not (re)load a dict if the passed in variable is
- % already a struct.
- %
- % You can specifiy a suffix to be appended to all pulse names in the
- % dictionary separated by a space, i.e. if the dictionary name is
- % 'common' you can pass 'common d12' and '_d12' will be appended to each
- % pulse name.
-
- global plsdata
- delim = '___';
-
- if nargin < 2 || isempty(create_dict)
- create_dict = false;
- end
- create_dict = create_dict || isempty(dict_string_or_struct);
-
- if ischar(dict_string_or_struct)
- dict_string_or_struct = strsplit(dict_string_or_struct, ' ');
-
- if numel(dict_string_or_struct) > 1
- suffix = ['_' dict_string_or_struct{2}];
- else
- suffix = '';
- end
- dict_string_or_struct = dict_string_or_struct{1};
-
- file_name = fullfile(plsdata.dict.path, [dict_string_or_struct '.json']);
- if exist(file_name, 'file')
- text = fileread(file_name);
- dict_string_or_struct = jsondecode(text);
- dict_string_or_struct = qc.array2row(dict_string_or_struct);
- elseif create_dict
- if strcmp(suffix, '')
- dict_string_or_struct = struct(strcat('dict', delim, 'name'), dict_string_or_struct, 'global', struct());
- else
- error('Cannot create dictionary ''%s %s'' since it contains a space', dict_string_or_struct, suffix(2:end));
- end
- else
- error('Dictionary ''%s'' does not exist', dict_string_or_struct);
- end
-
- if ~strcmp(suffix, '')
- for fn = fieldnames(dict_string_or_struct)'
- if ~strcmp(fn{1}, strcat('dict', delim, 'name')) && ~strcmp(fn{1}, 'global')
- dict_string_or_struct.([fn{1} suffix]) = dict_string_or_struct.(fn{1});
- dict_string_or_struct = rmfield(dict_string_or_struct, fn{1});
- end
- end
- end
-
- end
-
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/load_pulse.m b/MATLAB/+qc/load_pulse.m
deleted file mode 100644
index 4c651bb92..000000000
--- a/MATLAB/+qc/load_pulse.m
+++ /dev/null
@@ -1,4 +0,0 @@
-function pulse = load_pulse(pulse_name)
-
- global plsdata
- pulse = plsdata.qc.pulse_storage{pulse_name};
\ No newline at end of file
diff --git a/MATLAB/+qc/operations_to_python.m b/MATLAB/+qc/operations_to_python.m
deleted file mode 100644
index 5790f0b5d..000000000
--- a/MATLAB/+qc/operations_to_python.m
+++ /dev/null
@@ -1,34 +0,0 @@
-function pyOperations = operations_to_python(operations)
- % Convert operations struct from matlab to python
-
- pyOperations = {};
-
- for k = 1:numel(operations)
-
- if numel(operations{k}) > 1
- args = operations{k}(2:end);
- else
- args = {};
- end
- switch(operations{k}{1})
- case 'AlgebraicMoment'
- pyOp = py.atsaverage.operations.AlgebraicMoment(args{:});
- case 'Downsample'
- pyOp = py.atsaverage.operations.Downsample(args{:});
- case 'Histogram'
- pyOp = py.atsaverage.operations.Histogram(args{:});
- case 'RepAverage'
- pyOp = py.atsaverage.operations.RepAverage(args{:});
- case 'RepeatedDownsample'
- pyOp = py.atsaverage.operations.RepeatedDownsample(args{:});
- otherwise
- error('Operation %s not recognized', operations{k}{1});
- end
- pyOperations{end+1} = pyOp;
-
- end
-
- pyOperations = py.list(pyOperations);
-
-
-
diff --git a/MATLAB/+qc/params_add_delim.m b/MATLAB/+qc/params_add_delim.m
deleted file mode 100644
index dca1ed276..000000000
--- a/MATLAB/+qc/params_add_delim.m
+++ /dev/null
@@ -1,9 +0,0 @@
-function parameters = params_add_delim(parameters, pulse_name)
- % Prefix all parameters in pulseTemplate with the pulse name followed by
- % three underscores (needed for dictionaries).
-
- delim = '___';
- for fn = fieldnames(parameters)'
- parameters.(strcat(pulse_name, delim, fn{1})) = parameters.(fn{1});
- parameters = rmfield(parameters, fn{1});
- end
\ No newline at end of file
diff --git a/MATLAB/+qc/params_rm_delim.m b/MATLAB/+qc/params_rm_delim.m
deleted file mode 100644
index 6a498e4af..000000000
--- a/MATLAB/+qc/params_rm_delim.m
+++ /dev/null
@@ -1,13 +0,0 @@
-function [parameters, pulse_name] = params_rm_delim(parameters)
- % Remove prefix from parameters identified by three underscores
- % (needed for dictionaries).
-
- delim = '___';
- fn = fieldnames(parameters)';
- [i1, i2] = regexp(fn{1}, '^.+___');
- pulse_name = fn{1}(i1:i2-numel(delim));
-
- for fn = fieldnames(parameters)'
- parameters.(fn{1}(i2+1:end)) = parameters.(fn{1});
- parameters = rmfield(parameters, fn{1});
- end
\ No newline at end of file
diff --git a/MATLAB/+qc/personalPaths_README.txt b/MATLAB/+qc/personalPaths_README.txt
deleted file mode 100644
index ee36d7590..000000000
--- a/MATLAB/+qc/personalPaths_README.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-README path file for qc.qctoolkitTestSetup
-------------------------------------------
-written by M. Meyer 10|2018
-
-
-Run the following code after inserting paths to create a path file. The generated file should be on the git ignore list.
-
-
-%% ----------------------------
-a = struct();
-a.pulses_repo = '';
-a.dicts_repo = '';
-a.loadPath = '';
-a.tunePath = '';
-a.loadFile = '';
-a.taborDriverPath = '';
-
-quPulsePath = '...\qc-toolkit\MATLAB\+qc';
-
-personalPathsStruct = a;
-save([pathFile_save_path '\personalPaths'], personalPathsStruct);
-%% -----------------------------
\ No newline at end of file
diff --git a/MATLAB/+qc/plot_program_tree.m b/MATLAB/+qc/plot_program_tree.m
deleted file mode 100644
index 960e12358..000000000
--- a/MATLAB/+qc/plot_program_tree.m
+++ /dev/null
@@ -1,60 +0,0 @@
-function plot_program_tree(program, maxElements, figId)
- % Modified from Wolfie at
- % https://stackoverflow.com/questions/45666560/ploting-a-nested-cell-as-a-tree-using-treeplot-matlab/45676012
-
- if nargin < 2 || isempty(maxElements)
- maxElements = 10;
- end
- if nargin < 3 || isempty(figId)
- figId = 120;
- end
-
- [treearray, nodeVals] = getTreeArray(program, maxElements);
-
- figure(figId); clf;
- treeplot(treearray);
- title(sprintf(['Duration %g' 10 'Showing first %g entries'], double(program.duration.numerator/program.duration.denominator), maxElements))
-
- % Get the position of each node on the plot
- [x, y] = treelayout(treearray);
-
- % Get the indices of the nodes which have values stored
- nodeIndices = cell2mat(nodeVals(1,:));
-
- % Get the labels (values) corresponding to those nodes. Must be strings in cell array
- labels = cellfun(@(x)(double(x.repetition_count)), nodeVals(2,:), 'uniformoutput', 0);
-
- % Add labels, with a vertical offset to the y coords so that labels don't sit on nodes
- text(x(nodeIndices), y(nodeIndices) - 0.03, labels);
-
-end
-
-function [treearray, nodeVals] = getTreeArray(program, maxElements)
- % Initialise the array construction from node 0
-
- children = program.children(1:min(maxElements, end));
-
- [nodes, ~, nodeVals] = treebuilder(children, 1);
- treearray = [0, nodes];
- nodeVals(:, end+1) = {1; program};
-
- % Recursive tree building function
- function [nodes, currentNode, nodeVals] = treebuilder(children, rootNode)
- % Set up variables to be populated whilst looping
- nodes = []; nodeVals = {};
-
- % Start node off at root node
- currentNode = rootNode;
-
- % Loop over array elements, either recurse or add node
- for ii = 1:min(size(children, 2))
- currentNode = currentNode + 1;
- try
- nodeVals = [nodeVals, {currentNode; children{ii}}];
- [subtreeNodes, currentNode, newNodeVals] = treebuilder(children{ii}.children, currentNode);
- nodes = [nodes, rootNode, subtreeNodes];
- nodeVals = [nodeVals, newNodeVals];
- end
- end
- end
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/plot_pulse.m b/MATLAB/+qc/plot_pulse.m
deleted file mode 100644
index f63c67b42..000000000
--- a/MATLAB/+qc/plot_pulse.m
+++ /dev/null
@@ -1,178 +0,0 @@
-function [t, channels, measurements, instantiatedPulse] = plot_pulse(pulse, varargin)
-
- global plsdata
-
- defaultArgs = struct(...
- 'sample_rate', plsdata.awg.sampleRate, ... % in 1/s, converted to 1/ns below
- 'channel_names', {{}}, ... % names of channels to plot, all if empty
- 'parameters', struct(), ...
- 'channel_mapping', [], ...
- 'window_mapping' , py.None, ...
- 'fig_id', plsdata.qc.figId, ...
- 'subplots', [121 122], ...
- 'charge_diagram_data', {{}}, ... % inputs to imagesc
- 'clear_fig', true, ...
- 'charge_diagram', {{'X', 'Y'}}, ...
- 'plot_charge_diagram', true, ...
- 'lead_points', 1e-3*[-4 -1; -1 -2; 0 -4; 4 0; 2 1; 1 4], ...
- 'special_points', struct('M', [0 0], 'R1', [-2.5e-3 -3.75e-3], 'R2', [-2e-3 1e-3], 'S', [-2e-3 -1e-3], 'Tp', [1.75e-3 0], 'STp', [1e-3 -1e-3]), ...
- 'plot_range', [-8e-3 8e-3], ...
- 'max_n_points', 1e5,...
- 'dont_plot', false ...
- );
-
- args = util.parse_varargin(varargin, defaultArgs);
-
- if isempty(args.channel_mapping)
- args.channel_mapping = py.dict(py.zip(pulse.defined_channels, pulse.defined_channels));
- end
-
- args.sample_rate = args.sample_rate * 1e-9; % convert to 1/ns
- instantiatedPulse = qc.instantiate_pulse(pulse, 'parameters', args.parameters, 'channel_mapping', args.channel_mapping, 'window_mapping', args.window_mapping);
-
- if ~qc.is_instantiated_pulse(pulse)
- nPoints = qc.get_pulse_duration(pulse, args.parameters) * args.sample_rate * 1e9;
- if nPoints > args.max_n_points
- warning('Number of points %g > %g (maximum number of points). Aborting.\n', ceil(nPoints), args.max_n_points);
- return
- end
- end
-
- try
- data = util.py.py2mat(py.qctoolkit.pulses.plotting.render(instantiatedPulse, pyargs('sample_rate', args.sample_rate, 'render_measurements', true)));
- catch err
- warning('The following error occurred when plotting. This might have to do with Python dicts not being convertable to Matlab because of illegal struct field name:\n%s', err.getReport())
- end
-
- t = data{1}*1e-9;
-
- channels = data{2};
- if ~isempty(args.plot_range)
- for chan_name = fieldnames(channels)'
- channels.(chan_name{1}) = util.clamp(channels.(chan_name{1}), args.plot_range);
- end
- end
- measurements = struct();
- for m = data{3}
- if ~isfield(measurements, m{1}{1})
- measurements.(m{1}{1}) = [];
- end
- if strcmp(class(m{1}{2}), 'py.fractions.Fraction')
- m{1}{2} = m{1}{2}.numerator/m{1}{2}.denominator;
- end
- if strcmp(class(m{1}{3}), 'py.fractions.Fraction')
- m{1}{3} = m{1}{3}.numerator/m{1}{3}.denominator;
- end
- measurements.(m{1}{1})(end+1, 1:2) = [m{1}{2} m{1}{2}+m{1}{3}] * 1e-9;
- end
-
- if args.dont_plot
- return;
- end
-
- plotChargeDiagram = args.plot_charge_diagram && ~isempty(args.charge_diagram) && all(cellfun(@(x)(isfield(channels, x)), args.charge_diagram));
-
- hFig = figure(args.fig_id);
- if ~qc.is_instantiated_pulse(pulse)
- pulseName = sprintf('Pulse: %s', char(pulse.identifier));
- else
- pulseName = 'Pulse';
- end
- set(hFig, 'Name', pulseName);
- if args.clear_fig
- clf
- end
- if plotChargeDiagram || numel(args.subplots) == 1
- subplot(args.subplots(1));
- end
- hold on
-
- legendEntries = {};
- legendHandles = [];
-
- for meas_name = fieldnames(measurements)'
- hLines = plot(measurements.(meas_name{1}).', measurements.(meas_name{1}).'*0, 'lineWidth', 100, 'displayName', ['Meas: ' meas_name{1}]);
- for h = hLines(:)'
- color = rgb2hsv(h.Color);
- h.Color = hsv2rgb([color(1) 0.1 1]);
- end
- legendHandles(end+1) = h(1);
- legendEntries{end+1} = ['Meas: ' meas_name{1}];
- end
-
- for chan_name = fieldnames(channels)'
- if isempty(args.channel_names) || any(cellfun(@(x)(strcmp(x, chan_name{1})), args.channel_names))
- h = util.rectplot(t, channels.(chan_name{1}), '.-');
- legendHandles(end+1) = h(1);
- legendEntries{end+1} = ['Chan: ' chan_name{1}];
- end
- end
-
- if ~isempty(args.plot_range)
- title(['Plot range: ' sprintf('%g ', args.plot_range)]);
- end
- xlabel('t(s)');
- [~, hObj] = legend(legendHandles, legendEntries);
- hObj = findobj(hObj, 'type', 'line');
- set(hObj, 'lineWidth', 2);
-
- if plotChargeDiagram
- subplot(args.subplots(2));
- hold on
- ax = gca;
- userData = get(ax, 'userData');
- if ~isempty(args.plot_range)
- title(['Plot range: ' sprintf('%g ', args.plot_range)]);
- end
-
- if ~isempty(args.charge_diagram_data)
- if numel(size(squeeze(args.charge_diagram_data{3}))) == 3
- args.charge_diagram_data{3} = squeeze(nanmean(args.charge_diagram_data{3},1));
- end
- imagesc(args.charge_diagram_data{:});
- end
-
- if isempty(userData) || ~isstruct(userData) || ~isfield(userData, 'leadsPlotted') || ~userData.leadsPlotted
- color = [0 0 0 0.1];
- lineWidth = 3;
-
- if ~isempty(args.lead_points)
- plot(args.lead_points(1:3,1), args.lead_points(1:3,2), '-', 'lineWidth', lineWidth, 'color', color);
- plot(args.lead_points(4:6,1), args.lead_points(4:6,2), '-', 'lineWidth', lineWidth, 'color', color);
- plot(args.lead_points([2 5],1), args.lead_points([2 5],2), '--', 'lineWidth', lineWidth, 'color', color);
-
- offset = abs(max(args.lead_points(:))-min(args.lead_points(:)))*0.05;
- else
- offset = 4e-4;
- end
-
- for name = fieldnames(args.special_points)'
- xy = args.special_points.(name{1});
- plot(xy(1), xy(2), 'g.', 'markerSize', 24, 'color', color);
- text(xy(1), xy(2)+offset, name{1}, 'horizontalAlignment', 'center', 'color', color);
- end
-
- set(gca, 'UserData', struct('leadsPlotted', true));
- end
-
- x = channels.(args.charge_diagram{1});
- y = channels.(args.charge_diagram{2});
-
- ax.ColorOrderIndex = 1;
- lineWidth = 1;
- h = plot(x, y, '.-', 'markerSize', 8, 'lineWidth', lineWidth);
- plot(x(1), y(1), 's', 'markerSize', 12, 'color', h.Color, 'lineWidth', lineWidth);
-
- dx = diff(x);
- dy = diff(y);
- r = sqrt(dx.^2 + dy.^2);
-
- nArrows = min(numel(r), floor(sum(r)/0.5e-3));
- [~, ind] = sort(r);
- ind = ind(end-nArrows+1:end);
- ind = sort(ind);
-
- quiver(x(ind), y(ind), dx(ind), dy(ind), 0, 'color', h.Color, 'lineWidth', lineWidth);
- end
-
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/plot_pulse_4chan.m b/MATLAB/+qc/plot_pulse_4chan.m
deleted file mode 100644
index 61680fb99..000000000
--- a/MATLAB/+qc/plot_pulse_4chan.m
+++ /dev/null
@@ -1,80 +0,0 @@
-function [t, channels, measurements, instantiatedPulse] = plot_pulse_4chan(pulse, varargin)
-% PLOT_PULSE_4CHAN Wrapper for plot_pulse specific for plotting pulses for
-% two qubits with two control channels each
-%
-% (c) 2018/06 Pascal Cerfontaine (cerfontaine@physik.rwth-aachen.de)
-
-defaultArgs = struct(...
- 'charge_diagram_data_structs', {{}}, ... Should contain 2 structs in a cell array with fields x, y
- ... and data, where data{1} contains the charge diagram data
- 'plot_charge_diagram', true, ...
- 'lead_points_cell', {{}}, ... Should contain a cell with a lead_points entry for each qubit
- 'special_points_cell', {{}}, ... Should contain a cell with a special_points entry for each qubit
- 'channels', {{'W', 'X', 'Y', 'Z'}}, ...
- 'measurements', {{'A', 'A', 'B', 'B'}}, ...
- 'markerChannels', {{'M1', '', 'M2', ''}} ...
- );
-args = util.parse_varargin(varargin, defaultArgs);
-
-
- for chrgInd = 1:2
- k = chrgInd + double(chrgInd==2);
- q = 4 - k;
-
- if numel(args.charge_diagram_data_structs) >= chrgInd
- args.charge_diagram_data = args.charge_diagram_data_structs{chrgInd};
- args.charge_diagram_data = {args.charge_diagram_data.x, args.charge_diagram_data.y, args.charge_diagram_data.data{1}};
- else
- args.charge_diagram_data = {};
- end
-
- if numel(args.lead_points_cell) >= chrgInd
- args.lead_points = args.lead_points_cell{chrgInd};
- else
- args.lead_points = {};
- end
-
- if numel(args.special_points_cell) >= chrgInd
- args.special_points = args.special_points_cell{chrgInd};
- else
- args.special_points = {};
- end
-
- args.charge_diagram = args.channels(k:k+1);
- if args.plot_charge_diagram
- args.subplots = [220+k 220+k+1];
- else
- args.subplots = [210+chrgInd];
- end
- args.clear_fig = k==1;
- [t, channels, measurements, instantiatedPulse] = qc.plot_pulse(pulse, args);
- xlabel(args.channels(k));
- ylabel(args.channels(k+1));
-
- if args.plot_charge_diagram
- subplot(args.subplots(1));
- end
- set(findall(gca, 'DisplayName', sprintf('Chan: %s', args.channels{q})), 'Visible', 'off');
- set(findall(gca, 'DisplayName', sprintf('Chan: %s', args.channels{q+1})), 'Visible', 'off');
- set(findall(gca, 'DisplayName', sprintf('Chan: %s', args.markerChannels{q})), 'Visible', 'off');
- set(findall(gca, 'DisplayName', sprintf('Chan: %s', args.markerChannels{q+1})), 'Visible', 'off');
- set(findall(gca, 'DisplayName', sprintf('Meas: %s', args.measurements{q})), 'Visible', 'off');
- set(findall(gca, 'DisplayName', sprintf('Meas: %s', args.measurements{q+1})), 'Visible', 'off');
-
- [hLeg, hObj] = legend(gca);
- for l = 1:numel(hLeg.String)
- if strcmp(hLeg.String{l}, sprintf('Chan: %s', args.channels{q})) || ...
- strcmp(hLeg.String{l}, sprintf('Chan: %s', args.channels{q+1})) || ...
- strcmp(hLeg.String{l}, sprintf('Chan: %s', args.markerChannels{q})) || ...
- strcmp(hLeg.String{l}, sprintf('Chan: %s', args.markerChannels{q+1})) || ...
- strcmp(hLeg.String{l}, sprintf('Meas: %s', args.measurements{q})) || ...
- strcmp(hLeg.String{l}, sprintf('Meas: %s', args.measurements{q+1}))
- hLeg.String{l} = '';
- end
- findobj(hObj, 'type', 'line');
- set(hObj, 'lineWidth', 2);
- end
-
-
-
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/plot_tabor_pulse.m b/MATLAB/+qc/plot_tabor_pulse.m
deleted file mode 100644
index c6355d39e..000000000
--- a/MATLAB/+qc/plot_tabor_pulse.m
+++ /dev/null
@@ -1,23 +0,0 @@
-function plot_tabor_pulse(awg)
-
-program = awg.read_complete_program();
-
-wfs = util.py.py2mat(program.get_waveforms());
-reps = util.py.py2mat(program.get_repetitions());
-n_wfs = numel(wfs);
-
-f = figure;
-
-
-
-tabgroup = uitabgroup(mainfig, 'Position', [.05 .1 .9 .8]);
-
-for k = 1:n_wfs
- tab(k)=uitab(tabgroup,'Title', sprintf('Wf_%i', k));
-
- axes('parent',tab(k))
-
- plot(wfs{k});
-
- legend(sprintf('%i times', reps(k)));
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/program_to_struct.m b/MATLAB/+qc/program_to_struct.m
deleted file mode 100644
index dfa327195..000000000
--- a/MATLAB/+qc/program_to_struct.m
+++ /dev/null
@@ -1,36 +0,0 @@
-function program = program_to_struct(program_name, pulse_template, parameters_and_dicts, channel_mapping, window_mapping, global_transformation)
-
- if ischar(pulse_template)
- error('Variable ''pulse_template'' must not be of type char to make sure the correct pulse is saved. Please pass a pulse template.')
- end
-
- % Make sure all dictionaries are loaded so not just saving strings
- if ~iscell(parameters_and_dicts)
- parameters_and_dicts = {parameters_and_dicts};
- end
- parameters_and_dicts = cellfun(@qc.load_dict, parameters_and_dicts, 'UniformOutput', false);
-
- program = struct( ...
- 'program_name', program_name, ...
- 'pulse_template', qc.pulse_to_struct(pulse_template), ...
- 'parameters_and_dicts', {parameters_and_dicts}, ...
- 'channel_mapping', channel_mapping, ...
- 'global_transformation', global_transformation, ...
- 'window_mapping', window_mapping ...
- );
- program.pulse_duration = qc.get_pulse_duration(pulse_template, qc.join_params_and_dicts(program.parameters_and_dicts));
- program.added_to_pulse_duration = 0;
-
- for name = fieldnames(program.channel_mapping)'
- if strcmp(class(program.channel_mapping.(name{1})), 'py.NoneType')
- program.channel_mapping.(name{1}) = py.None;
- end
- end
-
- for name = fieldnames(program.window_mapping)'
- if strcmp(class(program.window_mapping.(name{1})), 'py.NoneType')
- program.window_mapping.(name{1}) = py.None;
- end
- end
-
-
\ No newline at end of file
diff --git a/MATLAB/+qc/pulse_add_delim.m b/MATLAB/+qc/pulse_add_delim.m
deleted file mode 100644
index a36e82d79..000000000
--- a/MATLAB/+qc/pulse_add_delim.m
+++ /dev/null
@@ -1,35 +0,0 @@
-function mapped_pulse_template = pulse_add_delim(pulse_template, pulse_name, set_pulse_identifier)
- % Prefix all parameters in pulseTemplate with the pulse name followed by
- % three underscores (needed for dictionaries).
-
- if nargin < 3 || isempty(set_pulse_identifier)
- set_pulse_identifier = true;
- end
-
- delim = '___';
- parameters = qc.get_pulse_params(pulse_template);
- parameter_mapping = cell(1, 2*numel(parameters));
- for k = 1:numel(parameters)
- parameter_mapping{1, 2*k-1} = parameters{k};
- parameter_mapping{1, 2*k} = strcat(pulse_name, delim, parameters{k});
- end
- parameter_mapping = struct(parameter_mapping{:});
-
- if set_pulse_identifier
- mapped_pulse_template = py.qctoolkit.pulses.MappingPT( ...
- pyargs( ...
- 'template', pulse_template, ...
- 'identifier', pulse_name, ...
- 'parameter_mapping', parameter_mapping, ...
- 'allow_partial_parameter_mapping', true ...
- ) ...
- );
- else
- mapped_pulse_template = py.qctoolkit.pulses.MappingPT( ...
- pyargs( ...
- 'template', pulse_template, ...
- 'parameter_mapping', parameter_mapping, ...
- 'allow_partial_parameter_mapping', true ...
- ) ...
- );
- end
\ No newline at end of file
diff --git a/MATLAB/+qc/pulse_seq.m b/MATLAB/+qc/pulse_seq.m
deleted file mode 100644
index d5b206af2..000000000
--- a/MATLAB/+qc/pulse_seq.m
+++ /dev/null
@@ -1,142 +0,0 @@
-function [pulse, args] = pulse_seq(pulses, varargin)
-% PULSE_SEQ Summary
-% Dynamically sequence qctoolkit pulses
-%
-% Note: All SequencePTs in this function are wrapped in a RepetitionPT
-% if wrap_in_repetition_pt is true since this might lead to a more efficien
-% efficient upload on the Tabor AWG according to Simon Humpohl. However, if
-% the upload aborts with the error
-% "The algorithm is not smart enough to make sequence tables shorter"
-% you can set wrap_in_repetition_pt to false to possibly reduce the
-% sequence table length. This will increase the AWG memory requirement.
-%
-% --- Outputs -------------------------------------------------------------
-% pulse : Sequenced pulse template
-% args : Struct of all input arguments, including pulses
-%
-% --- Inputs --------------------------------------------------------------
-% pulses : Cell of pulse identifiers or pulse templates in sequence order
-% varargin : Name-value pairs or struct
-%
-% -------------------------------------------------------------------------
-% (c) 2018/06 Pascal Cerfontaine (cerfontaine@physik.rwth-aachen.de)
- global plsdata
-
- defaultArgs = struct( ...
- 'repetitions', {num2cell(ones(1, numel(pulses)))}, ... % Repetition for each pulse
- 'wrap_in_repetition_pt', true, ... % Wrap each SequencePT in a RepetitionPT with counter 1
- 'outerRepetition', 1, ... % Repetition for entire pulse, set to NaN to use a RepetitionPT with on repetition.
- 'fill_time_min', NaN, ... % If not NaN, add fill_time = py.sympy.Max(fill_time, args.fill_time_min).
- 'fill_param', '', ... % Not empty: Automatically add fill_pulse to achieve total time given by this parameter.
- ... % 'auto': Determine fill time automatically for efficient upload on Tabor AWG
- 'fill_pulse_param', 'wait___t', ... % Name of pulse parameter to use for total fill time
- 'fill_pulse', 'wait', ... % Pulse template or identifier of pulse to use for filling (added to beginning of pulse sequence)
- 'measurements', [], ... % Empty: Do not define any additional readout.
- ... % Otherwise: Argument #1 to pyargs('measurements', #1) of SequencePT without fill
- 'prefix', '' , ... % Prefix to add to each pulse parameters
- 'identifier', '' , ... % Empty: Do not add an identifier
- ... % Otherwise: Name of the final pulse
- 'sampleRate', plsdata.awg.sampleRate, ... % In SI units (Sa/s),
- 'minSamples', plsdata.awg.minSamples, ... % Minimum number of samples for fill pulse
- 'sampleQuantum', plsdata.awg.sampleQuantum ... % Sample increments for fill pulse
- );
- args = util.parse_varargin(varargin, defaultArgs);
- args.pulses = pulses;
- args.sampleRate = args.sampleRate/1e9; % in GSa/s
-
- wrap_in_repetition_pt = args.wrap_in_repetition_pt;
- function pt = wrap(pt)
- if wrap_in_repetition_pt
- pt = py.qctoolkit.pulses.RepetitionPT(pt, 1);
- end
- end
-
- % Load and repeat pulses
- for k = 1:numel(pulses)
- if ischar(pulses{k})
- pulses{k} = qc.load_pulse(pulses{k});
- end
-
- if any(args.repetitions{k} ~= 1)
- pulses{k} = py.qctoolkit.pulses.RepetitionPT(pulses{k}, args.repetitions{k});
- else
- pulses{k} = wrap(pulses{k});
- end
- end
-
- % Sequence pulses
- if ~isempty(args.measurements)
- pulse = wrap(py.qctoolkit.pulses.SequencePT(pulses{:}, pyargs('measurements', args.measurements)));
- else
- pulse = wrap(py.qctoolkit.pulses.SequencePT(pulses{:}));
- end
-
- % Add fill if fill_param not empty
- if ~isempty(args.fill_param)
- duration = py.getattr(pulse, 'duration');
- if qc.is_instantiated_pulse(args.fill_pulse)
- fill_pulse = args.fill_pulse;
- else
- fill_pulse = qc.load_pulse(args.fill_pulse);
- end
-
- minDuration = py.sympy.sympify(args.minSamples/args.sampleRate, pyargs('rational', py.True));
- durationQuantum = py.sympy.sympify(args.sampleQuantum/args.sampleRate, pyargs('rational', py.True));
- if strcmp(args.fill_param, 'auto')
- fill_time = ...
- py.sympy.Max( py.sympy.ceiling(duration.sympified_expression/durationQuantum)*durationQuantum, minDuration ) ...
- - duration.sympified_expression;
- else
- fill_time = py.sympy.Add(py.sympy.sympify(args.fill_param), -duration.sympified_expression);
- end
-
- if ~any(isnan(args.fill_time_min))
- fill_time = py.sympy.Max(fill_time, args.fill_time_min);
- end
-
- fill_pulse = py.qctoolkit.pulses.MappingPT( ...
- pyargs( ...
- 'template', fill_pulse, ...
- 'parameter_mapping', qc.dict(args.fill_pulse_param, fill_time), ...
- 'allow_partial_parameter_mapping', true ...
- ) ...
- );
- pulse = wrap(py.qctoolkit.pulses.SequencePT(fill_pulse, pulse));
- end
-
- % Add prefix to all pulse parameters (if not empty)
- if ~isempty(args.prefix)
- parameters = qc.get_pulse_params(pulse);
- parameter_mapping = cell(1, 2*numel(parameters));
- for k = 1:numel(parameters)
- parameter_mapping{1, 2*k-1} = parameters{k};
- parameter_mapping{1, 2*k} = strcat(args.prefix, parameters{k});
- end
-
- pulse = py.qctoolkit.pulses.MappingPT( ...
- pyargs( ...
- 'template', pulse, ...
- 'parameter_mapping', qc.dict(parameter_mapping{:}), ...
- 'allow_partial_parameter_mapping', true ...
- ) ...
- );
- end
-
- if any(isnan(args.outerRepetition))
- outerRepetition = 1;
- else
- outerRepetition = args.outerRepetition;
- end
-
- % Add pulse identifier if identifier not empty
- if ~isempty(args.identifier)
- pulse = py.qctoolkit.pulses.RepetitionPT( ...
- pulse, outerRepetition, ...
- pyargs('identifier', args.identifier) ...
- );
- elseif args.outerRepetition > 1
- pulse = py.qctoolkit.pulses.RepetitionPT( ...
- pulse, args.outerRepetition);
- end
-
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/pulse_to_struct.m b/MATLAB/+qc/pulse_to_struct.m
deleted file mode 100644
index 6d952fd61..000000000
--- a/MATLAB/+qc/pulse_to_struct.m
+++ /dev/null
@@ -1,15 +0,0 @@
-function pulseStruct = pulse_to_struct(pulseTemplate)
-
- backend = py.qctoolkit.serialization.DictBackend();
- serializer = py.qctoolkit.serialization.Serializer(backend);
-
- serializer.serialize(pulseTemplate);
- pulseStruct = util.py.py2mat(backend.storage);
-
- if ~isfield(pulseStruct, 'main')
- pulseStruct.main = char(pulseTemplate.identifier);
- end
-
-
-
-
\ No newline at end of file
diff --git a/MATLAB/+qc/qcToolkitTestSetupUploadProgram.m b/MATLAB/+qc/qcToolkitTestSetupUploadProgram.m
deleted file mode 100644
index 44b3a34b1..000000000
--- a/MATLAB/+qc/qcToolkitTestSetupUploadProgram.m
+++ /dev/null
@@ -1,67 +0,0 @@
-% set upload pulse
-% upload_pulse = 'dnp_wait_dbz_4chan';
-% upload_pulse = 'decay_j_fid_4chan';
-% upload_pulse = 's_pumping_AB_4chan';
-% upload_pulse = 'pumping_s_stp';
-% upload_pulse = 'pumping_s';
-upload_scan = 'dnp_decay_dbz_fid_4chan';
-% upload_scan = 'line';
-% upload_scan = 'lead';
-% upload_scan = 'tl';
-
-
-
-
-%% Just an example with the Tabor AWG simulator
-% awgctrl('default except offsets')
-plsdata.awg.inst.send_cmd(':OUTP:COUP:ALL HV');
-args = tunedata.run{1}.(upload_scan).opts(1).scan.configfn(4).args;
-args{end}.window_mapping.A = py.None;
-args{end}.window_mapping.B = py.None;
-args{end}.window_mapping.DBZFID_A = py.None;
-args{end}.window_mapping.DBZFID_B = py.None;
-args{end}.operations = {};
-feval(args{2:end});
-
-
-%% test scan to test if why one can not upload pump pulses created with qctk concatenate
-myname = 'dnp_ABCD_4chan'
-
-
-window_mapping = struct('A', py.None, 'B', py.None);
-
-myargs = struct('program_name', myname, ...
- 'pulse_template', myname, ...
- 'channel_mapping', struct('W', 'TABOR_A', 'X', 'TABOR_B', 'Y', 'TABOR_C', 'Z', 'TABOR_D', 'MTrig', 'TABOR_A_MARKER', 'M1', 'TABOR_B_MARKER', 'M2', 'TABOR_C_MARKER'), ...
- 'parameters_and_dicts', {{'common', 'common_d12', 'common_d34', 'common_d12 d12', 'common_d34 d34'}}, ...
- 'window_mapping', window_mapping ...
- );
-
-plsdata.awg.inst.send_cmd(':OUTP:COUP:ALL HV');
-myinput = {'awg_program', @qc.awg_program, 'add', myargs};
-feval(myinput{2:end});
-
-
-%% reset AWG
-awgctrl('default except offsets');
-
-%% check out sequ table wait_4chan
-entries = qc.get_sequence_table('wait_4chan', false)
-entries{1}{end}
-entries{1}{end-1}
-entries{1}{end-2}
-entries{1}{end-5}
-
-
-%% check seq table j_fid_4chan
-entries = qc.get_sequence_table('j_fid_4chan', false)
-entries{1}{end}
-entries{1}{end-1}
-entries{1}{end-2}
-entries{1}{end-5}
-
-%%
-entries = qc.get_sequence_table('wait_4chan', true)
-disp(entries{1}{1});
-disp(entries{2}{1});
-% disp(entries{3}{1});
\ No newline at end of file
diff --git a/MATLAB/+qc/qctoolkitTestSetup.m b/MATLAB/+qc/qctoolkitTestSetup.m
deleted file mode 100644
index 32226a141..000000000
--- a/MATLAB/+qc/qctoolkitTestSetup.m
+++ /dev/null
@@ -1,94 +0,0 @@
-%% --- Test setup without AWG and Alazar (only qctoolkit) -----------------
-quPulsePath = fileparts(which('qc.qctoolkitTestSetup'));
-load([quPulsePath '\personalPaths.mat']);
-global plsdata
-plsdata = struct( ...
- 'path', personalPathsStruct.pulses_repo, ...
- 'awg', struct('inst', [], 'hardwareSetup', [], 'sampleRate', 2e9, 'currentProgam', '', 'registeredPrograms', struct(), 'defaultChannelMapping', struct(), 'defaultWindowMapping', struct(), 'defaultParametersAndDicts', {{}}, 'defaultAddMarker', {{}}), ...
- 'dict', struct('cache', [], 'path', personalPathsStruct.dicts_repo), ...
- 'qc', struct('figId', 801), ...
- 'daq', struct('inst', [], 'defaultOperations', {{}}, 'reuseOperations', false, 'operationsExternallyModified', false) ...
- );
-plsdata.daq.instSmName = 'ATS9440Python';
-plsdata.qc.backend = py.qctoolkit.serialization.FilesystemBackend(plsdata.path);
-plsdata.qc.serializer = py.qctoolkit.serialization.Serializer(plsdata.qc.backend);
-% -------------------------------------------------------------------------
-
-%% --- Test setup replicating the Triton 200 measurement setup ------------
-% Does not replicate Alazar functionality as there is no simulator
-% Need the triton_200 repo on the path (for awgctrl)
-
-% Path for Triton 200 backups
-loadPath = personalPathsStruct.loadPath;
-pulsePath = plsdata.path;
-dictPath = plsdata.dict.path;
-tunePath = personalPathsStruct.tunePath;
-
-% Loading
-try
- copyfile(fullfile(loadPath, 'smdata_recent.mat'), fullfile(tunePath, 'smdata_recent.mat'));
-catch err
- warning(err.getReport());
-end
-load(fullfile(tunePath, 'smdata_recent.mat'));
-info = dir(fullfile(tunePath, 'smdata_recent.mat'));
-fprintf('Loaded smdata from %s\n', datestr(info.datenum));
-try
- copyfile(fullfile(loadPath, 'tunedata_recent.mat'), fullfile(tunePath, 'tunedata_recent.mat'));
-% copyfile(fullfile(loadPath, 'tunedata_2018_06_25_21_08.mat'), fullfile(tunePath, 'tunedata_recent.mat'));
-catch err
- warning(err.getReport());
-end
-load(fullfile(tunePath, 'tunedata_recent.mat'));
-info = dir(fullfile(tunePath, 'tunedata_recent.mat'));
-fprintf('Loaded tunedata from %s\n', datestr(info.datenum));
-
-try
- copyfile(fullfile(loadPath, 'plsdata_recent.mat'), fullfile(tunePath, 'plsdata_recent.mat'));
-catch err
- warning(err.getReport());
-end
-load(fullfile(tunePath, 'plsdata_recent.mat'));
-info = dir(fullfile(tunePath, 'plsdata_recent.mat'));
-fprintf('Loaded plsdata from %s\n', datestr(info.datenum));
-
-global tunedata
-global plsdata
-tunedata.run{tunedata.runIndex}.opts.loadFile = personalPathsStruct.loadPath;
-import tune.tune
-
-plsdata.path = pulsePath;
-
-% Alazar dummy instrument (simulator not implemented yet)
-smdata.inst(sminstlookup(plsdata.daq.instSmName)).data.address = 'simulator';
-plsdata.daq.inst = py.qctoolkit.hardware.dacs.alazar.AlazarCard([]);
-
-% Setup AWG
-% Turns on AWG for short time but turns it off again
-% Initializes hardware setup
-% Can also be used for deleting all programs/resetting but then also need to setup Alazar again, i.e. the cell above and the three cells below )
-plsdata.awg.hardwareSetup = [];
-qc.setup_tabor_awg('realAWG', false, 'simulateAWG', true, 'taborDriverPath', personalPathsStruct.taborDriverPath);
-
-% AWG default settings
-awgctrl('default');
-plsdata.awg.inst.send_cmd(':OUTP:COUP:ALL HV');
-
-% Alazar
-% Execute after setting up the AWG since needs hardware setup initialized
-% Need to test whether need to restart Matlab if execute
-% qc.setup_alazar_measurements twice
-qc.setup_alazar_measurements('nQubits', 2, 'nMeasPerQubit', 4, 'disp', true);
-
-% Qctoolkit
-plsdata.qc.backend = py.qctoolkit.serialization.FilesystemBackend(pulsePath);
-plsdata.qc.serializer = py.qctoolkit.serialization.Serializer(plsdata.qc.backend);
-plsdata.dict.path = dictPath;
-
-% Tune
-tunedata.run{tunedata.runIndex}.opts.path = tunePath;
-
-
-import tune.tune
-disp('done');
-% -------------------------------------------------------------------------
\ No newline at end of file
diff --git a/MATLAB/+qc/qctoolkit_programs.m b/MATLAB/+qc/qctoolkit_programs.m
deleted file mode 100644
index fe6b634fc..000000000
--- a/MATLAB/+qc/qctoolkit_programs.m
+++ /dev/null
@@ -1,55 +0,0 @@
-%% Charge scan
-
-%% 4 CHANs
-pulseLocation = 'C:\Users\lablocal\Documents\PYTHON\qc-toolkit-pulses';
-charge_scan_pulse = qctoolkit.load_pulse('general_charge_scan', pulseLocation);
-
-charge_scan_pulse_params = struct( ...
- 'N_y', 10, ...
- 't_wait', 0, ... % ns
- 'y_stop', 1, ...
- 'x_stop', 1, ...
- 'x_start', -1, ...
- 'N_x', 10, ...
- 't_meas', 192, ...
- 'y_start', -1, ...
- 'W_fast', 0, ...
- 'W_slow', 1, ...
- 'X_fast', 1, ...
- 'X_slow', 0, ...
- 'Y_fast', 0, ...
- 'Y_slow', 1, ...
- 'Z_fast', 0, ...
- 'Z_slow', 1, ...
- 'rep_count', 1 ...
- );
-plsdata.daq.inst.config.totalRecordSize = int64(0); % needed
-plsdata.daq.inst.config.aimedBufferSize = int64(2^24);
-plsdata.daq.inst.card.reset
-plsdata.daq.inst.update_settings = py.True;
-
-%%
-qc.plot_pulse(charge_scan_pulse, charge_scan_pulse_params)
-
-%% 4 CHANs
-operations = {...
- py.atsaverage.operations.Downsample('DS_A', 'A'),...
- py.atsaverage.operations.Downsample('DS_B', 'B')...
- ...py.atsaverage.operations.Downsample('DS_C', 'alazarC'),...
- ...py.atsaverage.operations.Downsample('DS_D', 'alazarD'),...
- };
-
-plsdata.daq.inst.register_operations('general_charge_scan', py.list(operations))
-
-%% 4 CHANs upload and arm
-qc.arm_program('general_charge_scan', chargeScanPulseParams, ...
- 'channel_mapping', struct('W', 'TABOR_A','X', 'TABOR_B','Y', 'TABOR_C', 'Z', 'TABOR_D', 'marker', 'TABOR_A_MARKER'), ...
- 'window_mapping', struct('A', 'A', 'B', 'B'),...
- 'update', true)
-
-%% Same as above if program already uploaded
-qc.arm_program('general_charge_scan');
-
-%% Start program
-awgctrl('run');
-
diff --git a/MATLAB/+qc/readme.txt b/MATLAB/+qc/readme.txt
deleted file mode 100644
index 10dddd2d3..000000000
--- a/MATLAB/+qc/readme.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-Test setup
------------------------------------------------------------------------------
-Use qctoolkitTestSetup
-
-
-Naming convention
------------------------------------------------------------------------------
-Since qctoolkit uses underscores in variable names (instead of camel case) this package uses underscores. Some functions also use camel case for all variables which do not map directly to qctoolkit variables (otherwise it uses the qctoolkit variable and thus underscores).
diff --git a/MATLAB/+qc/save_dict.m b/MATLAB/+qc/save_dict.m
deleted file mode 100644
index 771c8751b..000000000
--- a/MATLAB/+qc/save_dict.m
+++ /dev/null
@@ -1,18 +0,0 @@
-function save_dict(dict_struct)
- global plsdata
- delim = '___';
-
- if isstruct(dict_struct)
- if ~isfield(dict_struct, 'global')
- dict_struct.global = struct();
- end
- dict_struct = qc.array2list(dict_struct);
- text = py.json.dumps(dict_struct, pyargs('indent', int8(4), 'sort_keys', true));
- text = char(text);
- fileId = fopen(fullfile(plsdata.dict.path, [dict_struct.(strcat('dict', delim, 'name')) '.json']), 'w');
- fprintf(fileId, '%s', text);
- fclose(fileId);
- else
- error('Saving of dictionary failed since no struct was passed\n');
- end
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/save_pulse.m b/MATLAB/+qc/save_pulse.m
deleted file mode 100644
index 4cafdd4ef..000000000
--- a/MATLAB/+qc/save_pulse.m
+++ /dev/null
@@ -1,30 +0,0 @@
-function [ file_written ] = save_pulse( pulse_template, overwrite )
-
- global plsdata
-
- if nargin < 2 || isempty(overwrite)
- overwrite = true;
- end
-
- file_written = false;
-
- if py.operator.contains(plsdata.qc.pulse_storage, pulse_template.identifier)
- if overwrite
- py.operator.delitem(plsdata.qc.pulse_storage, pulse_template.identifier);
- else
- warning('Did not write file as it exists and overwrite == false');
- return;
- end
- end
-
- try
- plsdata.qc.pulse_storage{pulse_template.identifier} = pulse_template;
- file_written = true;
-% fprintf('File(s) written\n');
- catch err
- warning(err.getReport());
- end
-end
-
-
-
diff --git a/MATLAB/+qc/set_alazar_buffer_strategy.m b/MATLAB/+qc/set_alazar_buffer_strategy.m
deleted file mode 100644
index 921275e62..000000000
--- a/MATLAB/+qc/set_alazar_buffer_strategy.m
+++ /dev/null
@@ -1,58 +0,0 @@
-function set_alazar_buffer_strategy(strategy, varargin)
- % SET_ALAZAR_BUFFER_STRATEGY sets the strategy to determine the buffer
- % size the alazar card uses.
- % strategy =
- % {'force_buffer_size'|'avoid_single_buffer'|'one_buffer_per_window'|'none'}
- %
- % strategy = 'none'
- % Uses default from python
- %
- % strategy = 'force_buffer_size'
- % Requires the argument 'target_size'
- %
- % strategy = 'avoid_single_buffer'
- % Optional argument 'target_size'
- %
- % strategy = 'one_buffer_per_window'
- % Uses the gcd of all measurement window periods. Usefull for charge
- % scans with lots of averaging
-
- import py.qupulse.hardware.dacs.alazar.AvoidSingleBufferAcquisition
- import py.qupulse.hardware.dacs.alazar.OneBufferPerWindow
- import py.qupulse.hardware.dacs.alazar.ForceBufferSize
-
-
- global plsdata
-
- switch lower(strategy)
- case 'none'
- py_strategy = py.None;
-
- case 'force_buffer_size'
- args = util.parse_varargin(varargin{:});
- if ~isfield(args, 'target_size')
- error('qc:set_alazar_buffer_strategy:missing','The buffer strategy "force_buffer_size" requires "target_size".')
- end
- py_strategy = ForceBufferSize(struct2pyargs(args));
-
-
- case 'avoid_single_buffer'
- default_args = struct('target_size', py.int(2^22));
- args = util.parse_varargin(default_args, varargin{:});
- py_strategy = AvoidSingleBufferAcquisition(ForceBufferSize(struct2pyargs(args)));
-
- case 'one_buffer_per_window'
- py_strategy = py.qupulse.hardware.dacs.alazar.OneBufferPerWindow();
-
- otherwise
- error('qc:set_alazar_buffer_strategy:unknown', 'Unknown buffer strategy "%s"', strategy);
- end
-
- plsdata.daq.inst.buffer_strategy = py_strategy;
- fprintf('Set buffer strategy to %s.\n', char(py.repr(py_strategy)));
-end
-
-function py_args = struct2pyargs(s)
- c = [fieldnames(s), struct2cell(s)]';
- py_args = pyargs(c{:});
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/set_pumping_at_awg.m b/MATLAB/+qc/set_pumping_at_awg.m
deleted file mode 100644
index 5d8e7674b..000000000
--- a/MATLAB/+qc/set_pumping_at_awg.m
+++ /dev/null
@@ -1,108 +0,0 @@
-function set_pumping_at_awg(pumpingConfig, varargin)
-
-global plsdata
-
-defaultArgs = struct(...
- 'programName', plsdata.awg.currentProgam, ...
- 'turnOffAWG', true, ...
- 'speedUp', false ...
- );
-args = util.parse_varargin(varargin, defaultArgs);
-
-if ~args.speedUp
- seqTable = qc.get_sequence_table(args.programName, false);
-else
- seqTable = qc.get_sequence_table(args.programName, false, {'AB', 'CD'}, false, true);
-end
-seqTableCheck = true;
-report = '';
-pumpSubTab = seqTable{1}{end};
-
-
-
-
-
-%---------------- some checks ---------------------------------------------
-if ~args.speedUp
-
- %check if there are six entries for the three pumping types for each qubit
- if length(pumpSubTab) < 6
- seqTableCheck = false;
- report = ' -- There are not six waveforms at the end of the sequence table! They might be put together or not uploaded or not at the end of the sequence table. -- ';
- end
-
- %test if every waveform is different/has a different
- if seqTableCheck
- for i = 0:5
- for j = 0:5
- if (i~=j) && (pumpSubTab{end-i}{2} == pumpSubTab{end-j}{2})
- report = ' -- Not all waveforms for pumping (that are assumed to be different) are different to each other! -- ';
- seqTableCheck = false;
- end
- end
- end
- end
-
- %test if both channel pairs have the same pumping sequence table part
- for i = 1:6
- if seqTableCheck && ~isequal(seqTable{1}{end}{end-i+1}, seqTable{2}{end}{end-i+1})
- report = ' -- Not the same pumping configuration on both channel pairs of the AWG! -- ';
- seqTableCheck = false;
- end
- end
-
-end
-
-
-%------------- reading out the pumping configuration ----------------------
-
-if ~seqTableCheck
- warning(report);
-else
- if ~args.speedUp
- seqTable{1}{end}{end-5}{1} = pumpingConfig.n_s_AB;
- seqTable{1}{end}{end-4}{1} = pumpingConfig.n_t_AB;
- seqTable{1}{end}{end-3}{1} = pumpingConfig.n_cs_AB;
- seqTable{1}{end}{end-2}{1} = pumpingConfig.n_s_CD;
- seqTable{1}{end}{end-1}{1} = pumpingConfig.n_t_CD;
- seqTable{1}{end}{end-0}{1} = pumpingConfig.n_cs_CD;
- seqTable{2}{end}{end-5}{1} = pumpingConfig.n_s_AB;
- seqTable{2}{end}{end-4}{1} = pumpingConfig.n_t_AB;
- seqTable{2}{end}{end-3}{1} = pumpingConfig.n_cs_AB;
- seqTable{2}{end}{end-2}{1} = pumpingConfig.n_s_CD;
- seqTable{2}{end}{end-1}{1} = pumpingConfig.n_t_CD;
- seqTable{2}{end}{end-0}{1} = pumpingConfig.n_cs_CD;
- else
- seqTable{1}{end} = py.list(seqTable{1}{end});
- seqTable{2}{end} = py.list(seqTable{1}{end});
- for i = 0:5
- seqTable{1}{end}{end-i} = py.list(seqTable{1}{end}{end-i});
- seqTable{2}{end}{end-i} = py.list(seqTable{2}{end}{end-i});
- end
- seqTable{1}{end}{end-5}{1} = py.int(pumpingConfig.n_s_AB);
- seqTable{1}{end}{end-4}{1} = py.int(pumpingConfig.n_t_AB);
- seqTable{1}{end}{end-3}{1} = py.int(pumpingConfig.n_cs_AB);
- seqTable{1}{end}{end-2}{1} = py.int(pumpingConfig.n_s_CD);
- seqTable{1}{end}{end-1}{1} = py.int(pumpingConfig.n_t_CD);
- seqTable{1}{end}{end-0}{1} = py.int(pumpingConfig.n_cs_CD);
- seqTable{2}{end}{end-5}{1} = py.int(pumpingConfig.n_s_AB);
- seqTable{2}{end}{end-4}{1} = py.int(pumpingConfig.n_t_AB);
- seqTable{2}{end}{end-3}{1} = py.int(pumpingConfig.n_cs_AB);
- seqTable{2}{end}{end-2}{1} = py.int(pumpingConfig.n_s_CD);
- seqTable{2}{end}{end-1}{1} = py.int(pumpingConfig.n_t_CD);
- seqTable{2}{end}{end-0}{1} = py.int(pumpingConfig.n_cs_CD);
- end
-end
-
-if args.speedUp
- qc.set_sequence_table(args.programName, seqTable, false, {'AB', 'CD'}, false, true);
-else
- qc.set_sequence_table(args.programName, seqTable, false);
-end
-qc.change_armed_program(args.programName, args.turnOffAWG);
-
-if args.turnOffAWG
- disp('turned AWG off');
-end
-
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/set_sequence_table.m b/MATLAB/+qc/set_sequence_table.m
deleted file mode 100644
index 2b064cc18..000000000
--- a/MATLAB/+qc/set_sequence_table.m
+++ /dev/null
@@ -1,96 +0,0 @@
-function set_sequence_table(program_name, seq_table, advanced_seq_table_flag, awg_channel_pair_identifiers, verbosity, input_python_list)
-% SET_SEQUENCE_TABLE Manually override sequence table of program on Tabor AWG
-%
-% This only changes the sequence table in the associated Tabor channel
-% pairs in qctoolkit. In order to actually update the sequence table on the
-% AWG, you still need to run qc.change_armed_program(program_name, ...).
-%
-% --- Inputs --------------------------------------------------------------
-% program_name : Program name for which sequence table is set
-% seq_table : Cell of sequence table to set on each
-% Tabor channel pair. Empty elements will
-% not be set.
-% advanced_seq_table_flag : Set advanced sequence table if true.
-% Default is false.
-% awg_channel_pair_identifiers : Some substring in the channel pair
-% identifiers to be matched. Sequence tables
-% are sorted in the same order as channel
-% pair identifiers substrings passed in this
-% variable. Default is {'AB', 'CD'}.
-% verbosity : Print sequence table to command line.
-% Default is 0.
-% -------------------------------------------------------------------------
-% (c) 2018/06 Pascal Cerfontaine (cerfontaine@physik.rwth-aachen.de)
-
- global plsdata
- hws = plsdata.awg.hardwareSetup;
-
-
- if nargin < 3 || isempty(advanced_seq_table_flag)
- advanced_seq_table_flag = false;
- end
- if nargin < 4 || isempty(awg_channel_pair_identifiers)
- awg_channel_pair_identifiers = {'AB', 'CD'};
- end
- if nargin < 5 || isempty(verbosity)
- verbosity = 0;
- end
- if nargin <6 || isempty(input_python_list)
- input_python_list = false;
- end
-
- if ~input_python_list
- seq_table = int_typecast(seq_table);
- end
-
- known_awgs = util.py.py2mat(hws.known_awgs);
- sort_indices = cellfun(@(x)(find( cellfun(@(y)(~isempty(strfind(char(x.identifier), y))), awg_channel_pair_identifiers) )), known_awgs);
- known_awgs = known_awgs(sort_indices);
-
- assert(numel(seq_table) == length(known_awgs), 'Sequence table needs to be a cell with an element for each of the %i channel pairs.', length(known_awgs));
-
- for k = 1:numel(seq_table)
- known_programs{k} = util.py.py2mat(py.getattr(known_awgs{k}, '_known_programs'));
-
- if isfield(known_programs{k}, program_name) && ~isempty(seq_table{k})
- if input_python_list
- if advanced_seq_table_flag
- known_awgs{k}.set_program_advanced_sequence_table(program_name, seq_table{k});
- % known_awgs{k}.set_program_advanced_sequence_table(program_name, seq_table{k});
- else
- known_awgs{k}.set_program_sequence_table(program_name, seq_table{k}); % Since it has to be a list inside a list, but this list if list is only trivial if advanced seq table is trivial, otherwiese each entry can be called by advanced seq table
- % known_awgs{k}.set_program_sequence_table(program_name,seq_table{k});
- end
- else
- if advanced_seq_table_flag
- known_awgs{k}.set_program_advanced_sequence_table(program_name, py.list(seq_table{k}));
- % known_awgs{k}.set_program_advanced_sequence_table(program_name, seq_table{k});
- else
- known_awgs{k}.set_program_sequence_table(program_name, py.list(seq_table{k})); % Since it has to be a list inside a list, but this list if list is only trivial if advanced seq table is trivial, otherwiese each entry can be called by advanced seq table
- % known_awgs{k}.set_program_sequence_table(program_name,seq_table{k});
- end
- end
-
- end
- end
-
- if verbosity > 0
- qc.get_sequence_table(program_name, advanced_seq_table_flag, awg_channel_pair_identifiers, verbosity);
- end
-
-end
-
-
-function out = int_typecast(in)
-
- if iscell(in)
- out = {};
- for k = 1:numel(in)
- out{k} = int_typecast(in{k});
- end
- elseif ~isempty(in)
- out = int64(in);
- end
-
-end
-
\ No newline at end of file
diff --git a/MATLAB/+qc/setup_alazar_measurements.m b/MATLAB/+qc/setup_alazar_measurements.m
deleted file mode 100644
index 1642a72b9..000000000
--- a/MATLAB/+qc/setup_alazar_measurements.m
+++ /dev/null
@@ -1,140 +0,0 @@
-function [mask_prototypes, measurement_map, txt] = setup_alazar_measurements(varargin) %
- % This function assumes the the first nQubits Alazar channels are hooked
- % up to qubits, the rest are auxiliary channels.
- %
- % Overview over mapping of measurements
- % -----------------------------------------------------------------------
- % measurement name defined in pulse
- % >>> window mapping >>>
- % measurement name defined in hardware setup (1st argument of set_measurement)
- % >>> set_measurement >>>
- % measurement mask (2nd argument of set_measurement, 1st argument of register_mask_for_channel)
- % >>> register_mask_for_channel >>>
- % alazar harware channel (2nd argument of register_mask_for_channel
- % -----------------------------------------------------------------------
- %
- % Example manual configuration
- % -----------------------------------------------------------------------
- % import py.qctoolkit.hardware.setup.MeasurementMask
- % hws = plsdata.awg.hardwareSetup;
- % daq = plsdata.daq.inst;
- % any name, give as 2nd arg in window_mapping alazar mask name
- % hws.set_measurement('A', MeasurementMask(plsdata.daq.inst, 'A'));
- % hws.set_measurement('B', MeasurementMask(plsdata.daq.inst, 'B'));
- % hws.set_measurement('C', MeasurementMask(plsdata.daq.inst, 'C'));
- % hws.set_measurement('D', MeasurementMask(plsdata.daq.inst, 'D'));
- % hws.set_measurement('A_B', MeasurementMask(plsdata.daq.inst, 'A'));
- % hws.set_measurement('A_B', MeasurementMask(plsdata.daq.inst, 'B'));
- %
- % alazar mask name, real alazar hardware channel
- % daq.register_mask_for_channel('A', uint64(0));
- % daq.register_mask_for_channel('B', uint64(1));
- % daq.register_mask_for_channel('C', uint64(2));
- % daq.register_mask_for_channel('D', uint64(3));
- % -----------------------------------------------------------------------
-
- global plsdata
- hws = plsdata.awg.hardwareSetup;
- daq = plsdata.daq.inst;
-
- defaultArgs = struct( ...
- 'disp', true, ...
- 'nMeasPerQubit', 2, ...
- 'nQubits', 2 ...
- );
- args = util.parse_varargin(varargin, defaultArgs);
- nAlazarChannels = 4;
- nQubits = args.nQubits;
- nMeasPerQubit = args.nMeasPerQubit;
-
- py.setattr(hws, '_measurement_map', py.dict);
- py.setattr(daq, '_mask_prototypes', py.dict);
- warning('Removing measurement_map and measurement_map might break stuff if previously set. Needs testing.');
-
- for q = 1:nQubits
- for m = 1:nMeasPerQubit
- % qubitIndex, measIndex, hwChannel, auxFlag1
- add_meas_and_mask(q, m, q+nQubits-1, false);
- end
- end
-
- for a = 1:(nAlazarChannels-nQubits)
- for m = 1:nMeasPerQubit
- % qubitIndex, measIndex, hwChannel, auxFlag1
- add_meas_and_mask(a, m, a-1, true);
- end
- end
-
- if args.nQubits > nAlazarChannels
- warning('More than %i qubits not implemented at the moment since Alazar has only %i channels.', nAlazarChannels, nAlazarChannels);
- end
-
- if args.nQubits > 2
- warning('Simultaneous measurements for more than 2 qubits not implemented at the moment.');
- end
- if q == 2
- for m = 1:nMeasPerQubit
- % Q1 Q2 qubitIndex, measIndex, hwChannel, auxFlag1, secondQubitIndex, secondHwChannel, auxFlag2
- add_meas_and_mask(1, m, 2, false, 2, 3 , false);
- % A1 A2 qubitIndex, measIndex, hwChannel, auxFlag1, secondQubitIndex, secondHwChannel, auxFlag2
- add_meas_and_mask(1, m, 0, true, 2, 1 , true);
-
- % Q1 A1 qubitIndex, measIndex, hwChannel, auxFlag1, secondQubitIndex, secondHwChannel, auxFlag2
- add_meas_and_mask(1, m, 2, false, 1, 0 , true);
- % Q1 A2 qubitIndex, measIndex, hwChannel, auxFlag1, secondQubitIndex, secondHwChannel, auxFlag2
- add_meas_and_mask(1, m, 2, false, 2, 1 , true);
-
- % Q2 A1 qubitIndex, measIndex, hwChannel, auxFlag1, secondQubitIndex, secondHwChannel, auxFlag2
- add_meas_and_mask(2, m, 3, false, 1, 0 , true);
- % Q2 A2 qubitIndex, measIndex, hwChannel, auxFlag1, secondQubitIndex, secondHwChannel, auxFlag2
- add_meas_and_mask(2, m, 3, false, 2, 1 , true);
- end
- end
-
- [mask_prototypes, measurement_map, txt] = qc.get_alazar_measurements('disp', args.disp);
-
-end
-
-
-function add_meas_and_mask(qubitIndex, measIndex, hwChannel, auxFlag1, secondQubitIndex, secondHwChannel, auxFlag2)
- global plsdata
-
- if nargin < 5
- secondQubitIndex = [];
- end
-
- if nargin < 7
- auxFlag2 = false;
- end
-
- if auxFlag1
- name = 'Aux';
- else
- name = 'Qubit';
- end
-
- if auxFlag2
- name2 = 'Aux';
- else
- name2 = 'Qubit';
- end
-
- if ~isempty(secondQubitIndex)
- measName = sprintf('%s_%i_%s_%i_Meas_%i', name, qubitIndex, name2, secondQubitIndex, measIndex);
- maskName = sprintf('%s_%i_%s_%i_Meas_%i_Mask_%i', name, qubitIndex, name2, secondQubitIndex, measIndex, 1);
- maskName2 = sprintf('%s_%i_%s_%i_Meas_%i_Mask_%i', name, qubitIndex, name2, secondQubitIndex, measIndex, 2);
- else
- measName = sprintf('%s_%i_Meas_%i', name, qubitIndex, measIndex);
- maskName = sprintf('%s_%i_Meas_%i_Mask_%i', name, qubitIndex, measIndex, 1);
- end
-
- plsdata.awg.hardwareSetup.set_measurement(measName, py.qctoolkit.hardware.setup.MeasurementMask(plsdata.daq.inst, maskName), ~isempty(secondQubitIndex));
- plsdata.daq.inst.register_mask_for_channel(maskName, uint64(hwChannel));
-
- if ~isempty(secondQubitIndex)
- plsdata.awg.hardwareSetup.set_measurement(measName, py.qctoolkit.hardware.setup.MeasurementMask(plsdata.daq.inst, maskName2), true);
- plsdata.daq.inst.register_mask_for_channel(maskName2, uint64(secondHwChannel));
- end
-end
-
-
diff --git a/MATLAB/+qc/setup_tabor_awg.m b/MATLAB/+qc/setup_tabor_awg.m
deleted file mode 100644
index 7fb498f2c..000000000
--- a/MATLAB/+qc/setup_tabor_awg.m
+++ /dev/null
@@ -1,129 +0,0 @@
-function setup_tabor_awg(varargin)
-
- global smdata
- global plsdata
-
- defaultArgs = struct( ...
- 'realAWG', true, ...
- 'sampleVoltPerAwgVolt', [util.db('dB2F',-48)*2 util.db('dB2F',-48)*2 util.db('dB2F',-44)*2 util.db('dB2F',-48)*2], ... % 10^(-dB/20)*ImpedanceMismatch
- 'simulateAWG', true, ...
- 'smChannels', {{'RFA', 'RFB', 'RFC', 'RFD'}}, ...
- 'taborName', 'TaborAWG2184C', ...
- 'globalTransformation', [], ...
- 'ip', '169.254.40.2', ... %IP's: Triton 200: 169.254.40.2 Triton 400: 169.254.40.55
- 'dcMode', false, ...
- 'maxPulseWait', 60, ... % Maximum waiting time in s in qc.awg_program before arming DAQ again
- 'taborDriverPath', 'C:\Users\lablocal\Documents\PYTHON\TaborDriver\' ...
- );
- args = util.parse_varargin(varargin, defaultArgs);
- plsdata.awg.sampleVoltPerAwgVolt = args.sampleVoltPerAwgVolt;
- plsdata.awg.dcMode = args.dcMode;
- plsdata.awg.triggerStartTime = 0;
- plsdata.awg.maxPulseWait = args.maxPulseWait;
- plsdata.awg.minSamples = 192;
- plsdata.awg.sampleQuantum = 16;
- plsdata.awg.globalTransformation = args.globalTransformation;
-
- for k = 1:numel(args.smChannels)
- smChannel = args.smChannels(k);
- if ~(smdata.channels(smchanlookup(smChannel)).instchan(1) == sminstlookup(args.taborName))
- error('Channel %s does not belong to %s\n', smChannel, args.taborName);
- end
- smdata.channels(smchanlookup(smChannel)).rangeramp(end) = 1/args.sampleVoltPerAwgVolt(k);
- end
-
- % Reload qctoolkit tabor AWG integration
- qctoolkit_tabor = py.importlib.reload(py.importlib.import_module('qctoolkit.hardware.awgs.tabor'));
-
- % Start simulator
- if args.simulateAWG
- if py.pytabor.open_session('127.0.0.1') == py.None
- dos([fullfile(args.taborDriverPath, 'WX2184C.exe') ' /switch-on /gui-in-tray&'])
-
- while py.pytabor.open_session('127.0.0.1') == py.None
- pause(1);
- disp('Waiting for Simulator to start...');
- end
- disp('Simulator started');
- end
- end
-
- if args.realAWG && ~args.simulateAWG
- % Only real instrument
- smdata.inst(sminstlookup(args.taborName)).data.tawg = qctoolkit_tabor.TaborAWGRepresentation(['TCPIP::' args.ip '::5025::SOCKET'], pyargs('reset', py.True));
- smdata.inst(sminstlookup(args.taborName)).data.tawg.paranoia_level = int64(2);
- elseif args.realAWG && args.simulateAWG
- % Simulator and real instrument
- smdata.inst(sminstlookup(args.taborName)).data.tawg = qctoolkit_tabor.TaborAWGRepresentation(['TCPIP::' args.ip '::5025::SOCKET'], pyargs('reset', py.True, 'mirror_addresses', {'127.0.0.1'}));
- elseif ~args.realAWG && args.simulateAWG
- % Just simulator
- smdata.inst(sminstlookup(args.taborName)).data.tawg = qctoolkit_tabor.TaborAWGRepresentation('TCPIP::127.0.0.1::5025::SOCKET', pyargs('reset', py.True));
- end
-
- plsdata.awg.inst = smdata.inst(sminstlookup(args.taborName)).data.tawg;
- if args.realAWG && exist('awgctrl.m', 'file')
- awgctrl('off');
- end
-
- % Create hardware setup for qctoolkit integration
- plsdata.awg.hardwareSetup = py.qctoolkit.hardware.setup.HardwareSetup();
-
- % Create python lambda function in Matlab
- numpy = py.importlib.import_module('numpy');
- for k = 1:numel(args.sampleVoltPerAwgVolt)
- multiply{k} = py.functools.partial(numpy.multiply, double(1./(args.sampleVoltPerAwgVolt(k))));
- end
-
- if args.realAWG || args.simulateAWG
- % PlaybackChannels can take more than two values (analog channels)
- plsdata.awg.hardwareSetup.set_channel('TABOR_A', ...
- py.qctoolkit.hardware.setup.PlaybackChannel(plsdata.awg.inst.channel_pair_AB, int64(0), multiply{1}));
- plsdata.awg.hardwareSetup.set_channel('TABOR_B', ...
- py.qctoolkit.hardware.setup.PlaybackChannel(plsdata.awg.inst.channel_pair_AB, int64(1), multiply{2}));
- plsdata.awg.hardwareSetup.set_channel('TABOR_C', ...
- py.qctoolkit.hardware.setup.PlaybackChannel(plsdata.awg.inst.channel_pair_CD, int64(0), multiply{3}));
- plsdata.awg.hardwareSetup.set_channel('TABOR_D', ...
- py.qctoolkit.hardware.setup.PlaybackChannel(plsdata.awg.inst.channel_pair_CD, int64(1), multiply{4}));
-
- plsdata.awg.hardwareSetup.set_channel('TABOR_AB', ...
- py.qctoolkit.hardware.setup.PlaybackChannel(plsdata.awg.inst.channel_pair_AB, int64(0), multiply{1}), py.True);
- plsdata.awg.hardwareSetup.set_channel('TABOR_AB', ...
- py.qctoolkit.hardware.setup.PlaybackChannel(plsdata.awg.inst.channel_pair_AB, int64(1), multiply{2}), py.True);
-
-
- plsdata.awg.hardwareSetup.set_channel('TABOR_AC', ...
- py.qctoolkit.hardware.setup.PlaybackChannel(plsdata.awg.inst.channel_pair_AB, int64(0), multiply{1}), py.True);
- plsdata.awg.hardwareSetup.set_channel('TABOR_AC', ...
- py.qctoolkit.hardware.setup.PlaybackChannel(plsdata.awg.inst.channel_pair_CD, int64(0), multiply{3}), py.True);
-
- plsdata.awg.hardwareSetup.set_channel('TABOR_AD', ...
- py.qctoolkit.hardware.setup.PlaybackChannel(plsdata.awg.inst.channel_pair_AB, int64(0), multiply{1}), py.True);
- plsdata.awg.hardwareSetup.set_channel('TABOR_AD', ...
- py.qctoolkit.hardware.setup.PlaybackChannel(plsdata.awg.inst.channel_pair_CD, int64(1), multiply{4}), py.True);
-
- plsdata.awg.hardwareSetup.set_channel('TABOR_BC', ...
- py.qctoolkit.hardware.setup.PlaybackChannel(plsdata.awg.inst.channel_pair_AB, int64(1), multiply{2}), py.True);
- plsdata.awg.hardwareSetup.set_channel('TABOR_BC', ...
- py.qctoolkit.hardware.setup.PlaybackChannel(plsdata.awg.inst.channel_pair_CD, int64(0), multiply{3}), py.True);
-
- plsdata.awg.hardwareSetup.set_channel('TABOR_BD', ...
- py.qctoolkit.hardware.setup.PlaybackChannel(plsdata.awg.inst.channel_pair_AB, int64(1), multiply{2}), py.True);
- plsdata.awg.hardwareSetup.set_channel('TABOR_BD', ...
- py.qctoolkit.hardware.setup.PlaybackChannel(plsdata.awg.inst.channel_pair_CD, int64(1), multiply{4}), py.True);
-
- plsdata.awg.hardwareSetup.set_channel('TABOR_CD', ...
- py.qctoolkit.hardware.setup.PlaybackChannel(plsdata.awg.inst.channel_pair_CD, int64(0), multiply{3}), py.True);
- plsdata.awg.hardwareSetup.set_channel('TABOR_CD', ...
- py.qctoolkit.hardware.setup.PlaybackChannel(plsdata.awg.inst.channel_pair_CD, int64(1), multiply{4}), py.True);
-
-
- % MarkerChannel can only take on two values (digital channels)
- plsdata.awg.hardwareSetup.set_channel('TABOR_A_MARKER', ...
- py.qctoolkit.hardware.setup.MarkerChannel(plsdata.awg.inst.channel_pair_AB, int64(0)));
- plsdata.awg.hardwareSetup.set_channel('TABOR_B_MARKER', ...
- py.qctoolkit.hardware.setup.MarkerChannel(plsdata.awg.inst.channel_pair_AB, int64(1)));
- plsdata.awg.hardwareSetup.set_channel('TABOR_C_MARKER', ...
- py.qctoolkit.hardware.setup.MarkerChannel(plsdata.awg.inst.channel_pair_CD, int64(0)));
- plsdata.awg.hardwareSetup.set_channel('TABOR_D_MARKER', ...
- py.qctoolkit.hardware.setup.MarkerChannel(plsdata.awg.inst.channel_pair_CD, int64(1)));
- end
\ No newline at end of file
diff --git a/MATLAB/+qc/strrep.m b/MATLAB/+qc/strrep.m
deleted file mode 100644
index 318f3d6f9..000000000
--- a/MATLAB/+qc/strrep.m
+++ /dev/null
@@ -1,15 +0,0 @@
-function varargout = strrep(varargin)
- % Multiple string replacements:
- % varargin{k} is a cell which should look like
- % {cell of original strings, search string, replacement string, search string, replacement string, ...}
- %
- % If n arguments are received, n outputs are returned.
-
- for k = 1:numel(varargin)
- varargout{k} = varargin{k}{1};
- for l = 2:2:numel(varargin{k})
- varargout{k} = cellfun(@(x)(strrep(x, varargin{k}{l}, varargin{k}{l+1})), varargout{k}, 'UniformOutput', false);
- end
- end
-
-end
\ No newline at end of file
diff --git a/MATLAB/+qc/struct_to_pulse.m b/MATLAB/+qc/struct_to_pulse.m
deleted file mode 100644
index 9214f766a..000000000
--- a/MATLAB/+qc/struct_to_pulse.m
+++ /dev/null
@@ -1,15 +0,0 @@
-function pulseTemplate = struct_to_pulse(pulseStruct)
-
- backend = py.qctoolkit.serialization.DictBackend();
- serializer = py.qctoolkit.serialization.Serializer(backend);
-
- if startsWith(pulseStruct.main, '{')
- pulseName = 'main';
- else
- pulseName = pulseStruct.main;
- end
-
- backend.storage.update(pulseStruct)
-
- pulseTemplate = serializer.deserialize(pulseName);
- % plsStruct = util.py.py2mat(backend.storage)
\ No newline at end of file
diff --git a/MATLAB/+qc/to_transformation.m b/MATLAB/+qc/to_transformation.m
deleted file mode 100644
index 3f694146a..000000000
--- a/MATLAB/+qc/to_transformation.m
+++ /dev/null
@@ -1,18 +0,0 @@
-function transformation = to_transformation(mat_trafo)
-
- trafo_module = py.importlib.import_module('qctoolkit._program.transformation');
-
- if istable(mat_trafo)
- assert(size(mat_trafo, 1) == size(mat_trafo, 2));
- if isempty(mat_trafo.Properties.RowNames)
- mat_trafo.Properties.RowNames = mat_trafo.Properties.VariableNames;
- end
-
- data = util.py.mat2py(mat_trafo{:,:});
-
- transformation = trafo_module.LinearTransformation(data, mat_trafo.Properties.RowNames', mat_trafo.Properties.VariableNames);
- elseif isempty(mat_trafo)
- transformation = py.None;
- else
- error('invalid trafo type');
- end
\ No newline at end of file
diff --git a/MATLAB/+qc/workaround_4chan_program_errors.m b/MATLAB/+qc/workaround_4chan_program_errors.m
deleted file mode 100644
index d2907b2f3..000000000
--- a/MATLAB/+qc/workaround_4chan_program_errors.m
+++ /dev/null
@@ -1,51 +0,0 @@
-function workaround_4chan_program_errors(a)
- % For some 4 channel programs, running another 2 channel program which
- % has marker channels on both AWGs beforehand, leads to erroneous voltage
- % outputs, even though the (advanced) sequence tables stored by qctoolkit
- % do not change. This is true even if qctoolkit is forced to reupload
- % the sequence tables of the 4 channel program.
- %
- % To repdroduce this error reset the AWG, then run
- % EITHER
- % 1) tune('resp'): correct output
- % 2) tune('lead', 2): correct output
- % 3) tune('resp'): erroneous output - if omitted, 5) gives erroneous output
- % 4) tune('line', 1): correct output
- % 5) tune('resp'): correct output
- % OR
- % 1) tune('lead', 2): correct output
- % 2) tune('comp'): correct output
- % 3) tune('resp'): erroneous output
- %
- % I (Pascal) found out this can be circumvented by arming the erroneous
- % program and then arming the idle program manually. Next, the erroneous
- % program can be run and now yields the correct result.
- %
- % This bug has been fixed now by adding the following lines in tabor.py
- % self.device.send_cmd('SEQ:DEL:ALL')
- % self._sequencer_tables = []
- % self.device.send_cmd('ASEQ:DEL')
- % self._advanced_sequence_table = []
-
- warning('No longer needed since bug has been fixed');
-
- % global plsdata
- %
- % if ~strcmp(plsdata.awg.currentProgam, a.program_name) && (~isfield(a, 'arm_global_for_workaround_4chan_program_errors'))
- % tic
- %
- % hws = plsdata.awg.hardwareSetup;
- % known_awgs = util.py.py2mat(hws.known_awgs);
- %
- % for k = 1:numel(known_awgs)
- % if any(cellfun(@(x)(strcmp(x, a.program_name)), fieldnames(util.py.py2mat(py.getattr(known_awgs{k}, '_known_programs')))))
- % known_awgs{k}.change_armed_program(a.program_name);
- % end
- % end
- %
- % for k = 1:numel(known_awgs)
- % known_awgs{k}.change_armed_program(py.None);
- % end
- %
- % fprintf('qc.workaround_4chan_program_errors executed...took %.0fs\n', toc);
- % end
\ No newline at end of file
diff --git a/MATLAB/+qc/workaround_alazar_single_buffer_acquisition.m b/MATLAB/+qc/workaround_alazar_single_buffer_acquisition.m
deleted file mode 100644
index 0f9aaa2bb..000000000
--- a/MATLAB/+qc/workaround_alazar_single_buffer_acquisition.m
+++ /dev/null
@@ -1,23 +0,0 @@
-function workaround_alazar_single_buffer_acquisition()
- % the alazar acquisition might fail if there is just a single buffer
- % the functionality to work around that was moved to the attribute
- % 'buffer_strategy' of the qupulse driver object
-
- % Some workaround for a workaround - ask Simon
- global plsdata
- plsdata.daq.inst.config.totalRecordSize = int64(0);
-% plsdata.daq.inst.config.aimedBufferSize = int64(2^24);
-
-
-if true
- fprintf('qc.workaround_alazar_single_buffer_acquisition decidete to try one buffer per measurement window\n');
- plsdata.daq.inst.buffer_strategy = py.qupulse.hardware.dacs.alazar.OneBufferPerWindow();
-else
- % use default behaviour
- plsdata.daq.inst.buffer_strategy = py.None;
-end
-
- plsdata.daq.inst.card.reset
- plsdata.daq.inst.update_settings = py.True;
-
- fprintf('qc.workaround_alazar_single_buffer_acquisition executed\n');
\ No newline at end of file
diff --git a/MATLAB/+qctoolkit/arm_pulse.m b/MATLAB/+qctoolkit/arm_pulse.m
deleted file mode 100644
index 8c7e94af5..000000000
--- a/MATLAB/+qctoolkit/arm_pulse.m
+++ /dev/null
@@ -1,60 +0,0 @@
-function arm_pulse(pulse_name, hardware_setup, parameters, pulse_location, varargin)
-
-default_args = struct('channel_mapping', py.None, ...
- 'window_mapping', py.None,...
- 'update', false,...
- 'add_marker', {{}});
-
-args = util.parse_varargin(varargin, default_args);
-if ~iscell(args.add_marker)
- args.add_marker = {args.add_marker};
-end
-
-if py.list(hardware_setup.registered_programs.keys()).count(pulse_name) == 0 || args.update
-
-%% LOAD PULSE
-
-backend = py.qctoolkit.serialization.FilesystemBackend(pulse_location);
-
-serializer = py.qctoolkit.serialization.Serializer(backend);
-
-pulse_template = serializer.deserialize(pulse_name);
-
-%% ADD MARKER
-if ~isempty(args.add_marker)
-
- marker_pulse = py.qctoolkit.pulses.PointPT({{0, 1},...
- {pulse_template.duration, 1}}, args.add_marker);
- pulse_template = py.qctoolkit.pulses.AtomicMultiChannelPT(pulse_template, marker_pulse);
-
- for ii = 1:numel(args.add_marker)
- args.channel_mapping.(args.add_marker{ii}) = args.add_marker{ii};
- end
-
-end
-
-
-%% INSTANTIATE PULSE (plug in parameters)
-
-sequencer = py.qctoolkit.pulses.Sequencer();
-
-kwargs = pyargs('parameters', parameters,...
- 'channel_mapping', args.channel_mapping,...
- 'window_mapping', args.window_mapping);
-
-sequencer.push(pulse_template, kwargs)
-
-instantiated_pulse = sequencer.build();
-
-%% LOAD PROGRAM TO AWG
-hardware_setup.register_program(pulse_name, instantiated_pulse, pyargs('update', args.update));
-
-end
-
-hardware_setup.arm_program(pulse_name);
-
-%% debug
-% alazar = util.py.py2mat(py.getattr(hardware_setup, '_measurement_map')).A{1}.dac
-% prog = util.py.py2mat(py.getattr(alazar, '_registered_programs')).charge_scan
-%
-
diff --git a/MATLAB/+qctoolkit/convert_qctoolkit.m b/MATLAB/+qctoolkit/convert_qctoolkit.m
deleted file mode 100644
index 2af5787a5..000000000
--- a/MATLAB/+qctoolkit/convert_qctoolkit.m
+++ /dev/null
@@ -1,34 +0,0 @@
-function pulse_group = convert_qctoolkit(qct_output)
-% pulse_group = convert_qctoolkit(qct_output)
-%
-% Registers pulses and converts pulse group data obtained from qupulse.
-%
-% qct_output: The output tuple of the
-% PulseControlInterface.create_pulse_group() method.
-
-qct_pulses = qct_output{2};
-
-% Convert Python dicts of pulses to pulse control waveform pulse structs
-% and register them using plsreg. Remember index in pulse database.
-pulse_indices = zeros(size(qct_pulses, 2));
-for i = 1:size(qct_pulses, 2)
- pulse = struct(qct_pulses{i});
- pulse.name = arrayfun(@char, pulse.name);
- pulse.data = struct(pulse.data);
- pulse.data.marker = cell2mat(cell(pulse.data.marker));
- pulse.data.wf = cell2mat(cell(pulse.data.wf));
- pulse_indices(i) = plsreg(pulse);
-end
-
-% Convert Python dict of pulse group to pulse control struct.
-% Replace pulse indices in pulse_group.pulses with the indices of the
-% pulses in the pulse database (plsdata).
-pulse_group = struct(qct_output{1});
-pulse_group.chan = double(pulse_group.chan);
-pulse_group.name = arrayfun(@char, pulse_group.name);
-pulse_group.ctrl = arrayfun(@char, pulse_group.ctrl);
-pulse_group.nrep = cellfun(@double, cell(pulse_group.nrep));
-pulse_group.pulses = cellfun(@double, cell(pulse_group.pulses));
-for i = 1:size(pulse_group.pulses, 2)
- pulse_group.pulses(i) = pulse_indices(pulse_group.pulses(i) + 1);
-end
\ No newline at end of file
diff --git a/MATLAB/+qctoolkit/example_scan_no_alazar.m b/MATLAB/+qctoolkit/example_scan_no_alazar.m
deleted file mode 100644
index 695577a45..000000000
--- a/MATLAB/+qctoolkit/example_scan_no_alazar.m
+++ /dev/null
@@ -1,128 +0,0 @@
-% The alazar card in THIS example is not controled with qctoolkit and all
-% measurement windows defined in the pulses are ignored
-function scan = example_scan_no_alazar(hardware_setup, tawg)
-
-global smdata;
-
-%tawg.send_cmd(':INST:SEL 1');
-%tawg.send_cmd(':SOUR:MARK:SEL 1; :SOUR:MARK:VOLT:HIGH 1.2');
-%tawg.send_cmd(':SOUR:MARK:SEL 1; :SOUR:MARK:STAT OFF');
-
-
-%set_marker = @() tawg.send_cmd(':ENAB; :TRIG; :SOUR:MARK:SOUR WAVE; :SOUR:MARK:SEL 1; :SOUR:MARK:STAT ON');
-%reset_marker = @() tawg.send_cmd(':SOUR:MARK:SEL 1; :SOUR:MARK:STAT OFF');
-
-
-% use pulse from example files
-qctoolkit_location = what('+qctoolkit');
-pulse_name = 'table_template';
-pulse_location = fullfile(qctoolkit_location.path,...
- '..', '..', 'doc', 'source', 'examples', 'serialized_pulses');
-
-
-% create struct with parameters
-parameters.va = 0;
-parameters.vb = 0.5;
-parameters.ta = 192;
-parameters.tb = 4*19200 - 192;
-parameters.tend = parameters.tb + 192;
-
-pulse_length = parameters.tend / tawg.sample_rate(uint64(1));
-
-% we want to play the channel 'A' of the pulse on the channel 'TABOR_A' of
-% the hardware_setup.
-channel_mapping.A = 'TABOR_A';
-
-% For a pulse with measurement windows we can rename them here
-% window_mapping.meas_in_pulse = 'meas_name'
-
-
-%% Configure data acquisition with alazar card
-sm_alazar_channel = 'ATS1';
-sm_alazar_instrument = smchaninst(sm_alazar_channel);
-sm_alazar_instrument = sm_alazar_instrument(1);
-
-
-alazar_config = sm_setups.common.AlazarDefaultSettings();
-
-trigger_range = 1;
-trigger_level = 0.01;
-
-alazar_config.trigger_settings.source_1 = 'A';
-alazar_config.trigger_settings.level_1 = uint8(128 + 127* (trigger_level / trigger_range));
-alazar_config.trigger_settings.slope_1 = 'positive';
-
-switch alazar_config.clock_settings.samplerate
- case 'rate_100MSPS'
- alazar_sample_rate = 100e6;
- otherwise
- error('invalid sample rate (changing the sample rate possibly breaks clock sync)');
-end
-
-alazar_downsampling = 1;
-
-masks = {};
-masks{1}.type = 'Periodic Mask';
-masks{1}.begin = 0;
-masks{1}.end = alazar_downsampling;
-masks{1}.period = alazar_downsampling;
-masks{1}.channel = 'A';
-
-alazar_config.total_record_size = pulse_length * alazar_sample_rate;
-if abs(alazar_config.total_record_size - round(alazar_config.total_record_size)) > 1e-10
- error('total record size is no integer');
-end
-alazar_config.total_record_size = int64(round(alazar_config.total_record_size));
-data_points_per_pulse = alazar_config.total_record_size / alazar_downsampling;
-
-operations = {};
-operations{1}.type = 'DS';% downsampling
-operations{1}.mask = 1;
-
-alazar_config.masks = masks;
-alazar_config.operations = operations;
-
-
-scan.configfn(1).fn = @smaconfigwrap;
-scan.configfn(1).args = {smdata.inst(sm_alazar_instrument).cntrlfn [sm_alazar_instrument 0 99] [] [] alazar_config}; % upload config ?
-
-scan.configfn(2).fn = @smaconfigwrap;
-scan.configfn(2).args = {smdata.inst(sm_alazar_instrument).cntrlfn,[sm_alazar_instrument 0 5]}; % write/commit config
-
-% upload pulse to AWG
-scan.configfn(3).fn = @smaconfigwrap;
-scan.configfn(3).args = {@qctoolkit.arm_pulse,...
- pulse_name, hardware_setup, parameters, pulse_location...
- 'channel_mapping', channel_mapping,...
- 'update', true};
-
-scan.loops(1).setchan = [];
-scan.loops(1).npoints = data_points_per_pulse;
-scan.loops(1).rng = [];
-scan.loops(1).ramptime = 0; % = sample rate * mask.period
-
-scan.loops(1).trigfn.fn = @(awg) awg.send_cmd(':TRIG');
-scan.loops(1).trigfn.args = {tawg};
-
-scan.loops(2).setchan = [];
-scan.loops(2).getchan = {sm_alazar_channel}; % read out buffer
-scan.loops(2).npoints = 2;
-scan.loops(2).ramptime = [];
-scan.loops(2).rng = 1:2;
-
-scan.disp(1).loop = 2;
-scan.disp(1).channel = 1;
-scan.disp(1).dim = 1;
-scan.disp(2).loop = 2;
-scan.disp(2).channel = 1;
-scan.disp(2).dim = 2;
-
-
-% arm Alazar before each scan loop(1)
-scan.loops(2).prefn(1).fn = @smaconfigwrap;
-scan.loops(2).prefn(1).args = {smdata.inst(sm_alazar_instrument).cntrlfn,[sm_alazar_instrument 0 4]};
-
-% arm AWG before each scan loop(1) (not really necessary)
-scan.loops(2).prefn(2).fn = @smaconfigwrap;
-scan.loops(2).prefn(2).args = {@qctoolkit.arm_pulse, pulse_name, hardware_setup};
-
diff --git a/MATLAB/+qctoolkit/get_pulse_duration.m b/MATLAB/+qctoolkit/get_pulse_duration.m
deleted file mode 100644
index 4568f4b7c..000000000
--- a/MATLAB/+qctoolkit/get_pulse_duration.m
+++ /dev/null
@@ -1,17 +0,0 @@
-function pulse_length = get_pulse_duration(pulse, parameters)
-
-parameter_kwargs = cell2namevalpairs(fieldnames(parameters), struct2cell(parameters));
-pulse_length = pulse.duration.evaluate_numeric(pyargs(parameter_kwargs{:}))*1e-9;
-
-
-
-
-% delete..
-function cellarr = cell2namevalpairs(fieldnames, values)
- cellarr={};
- for ii = 1:numel(fieldnames)
- cellarr{end+1}=fieldnames{ii};
- cellarr{end+1}=values{ii};
- end
-
-
\ No newline at end of file
diff --git a/MATLAB/+qctoolkit/get_pulse_parameters.m b/MATLAB/+qctoolkit/get_pulse_parameters.m
deleted file mode 100644
index 31bd17713..000000000
--- a/MATLAB/+qctoolkit/get_pulse_parameters.m
+++ /dev/null
@@ -1,6 +0,0 @@
-function pulse_parameters = get_pulse_parameters(varargin)
-
- pulse_template = qctoolkit.load_pulse(varargin{:});
-
- pulse_parameters = util.py.py2mat(pulse_template.parameter_names);
-
\ No newline at end of file
diff --git a/MATLAB/+qctoolkit/instantiate_pulse.m b/MATLAB/+qctoolkit/instantiate_pulse.m
deleted file mode 100644
index 5d1c25240..000000000
--- a/MATLAB/+qctoolkit/instantiate_pulse.m
+++ /dev/null
@@ -1,18 +0,0 @@
-%% INSTANTIATE PULSE (plug in parameters)
-function instantiated_pulse = instantiate_pulse(pulse_template, parameters, varargin)
-
-default_args = struct(...
- 'channel_mapping', py.None,...
- 'window_mapping', py.None);
-
-args = util.parse_varargin(varargin, default_args);
-
-sequencer = py.qctoolkit.pulses.Sequencer();
-
-kwargs = pyargs('parameters', parameters,...
- 'channel_mapping', args.channel_mapping,...
- 'window_mapping', args.window_mapping);
-
-sequencer.push(pulse_template, kwargs)
-
-instantiated_pulse = sequencer.build();
\ No newline at end of file
diff --git a/MATLAB/+qctoolkit/load_pulse.m b/MATLAB/+qctoolkit/load_pulse.m
deleted file mode 100644
index 4beb2f7e4..000000000
--- a/MATLAB/+qctoolkit/load_pulse.m
+++ /dev/null
@@ -1,7 +0,0 @@
-function pulse_template = load_pulse(pulse_name, pulse_location)
-
-backend = py.qctoolkit.serialization.FilesystemBackend(pulse_location);
-
-serializer = py.qctoolkit.serialization.Serializer(backend);
-
-pulse_template = serializer.deserialize(pulse_name);
\ No newline at end of file
diff --git a/MATLAB/+qctoolkit/plot_pulse.m b/MATLAB/+qctoolkit/plot_pulse.m
deleted file mode 100644
index 296aea689..000000000
--- a/MATLAB/+qctoolkit/plot_pulse.m
+++ /dev/null
@@ -1,30 +0,0 @@
-function plot_pulse(pulse, parameters, npoints)
- %%
-sequencer = py.qctoolkit.pulses.Sequencer();
-
-kwargs = pyargs('parameters', parameters);
-
-sequencer.push(pulse, kwargs);
-
-instantiated_pulse = sequencer.build();
-
-pulse_duration_in_s = qctoolkit.get_pulse_duration(pulse, parameters);
-pulse_duration_in_ns = pulse_duration_in_s * 1e9;
-%%
-if nargin < 3
- npoints = 100;
-end
-%%
-sample_rate = npoints / pulse_duration_in_ns;
-%%
-data = util.py.py2mat(py.qctoolkit.pulses.plotting.render(instantiated_pulse, pyargs('sample_rate', sample_rate)));
-
-t = data{1};
-figure;
-hold on
-
-for chan_name=fieldnames(data{2})'
- plot(t, data{2}.(chan_name{1}));
-end
-
-legend(fieldnames(data{2})');
\ No newline at end of file
diff --git a/MATLAB/+qctoolkit/plot_tabor_pulse.m b/MATLAB/+qctoolkit/plot_tabor_pulse.m
deleted file mode 100644
index 20da1dd53..000000000
--- a/MATLAB/+qctoolkit/plot_tabor_pulse.m
+++ /dev/null
@@ -1,23 +0,0 @@
-function plot_current_pulse(awg)
-
-program = awg.read_complete_program();
-
-wfs = util.py.py2mat(program.get_waveforms());
-reps = util.py.py2mat(program.get_repetitions());
-n_wfs = numel(wfs);
-
-f = figure;
-
-
-
-tabgroup = uitabgroup(mainfig, 'Position', [.05 .1 .9 .8]);
-
-for k = 1:n_wfs
- tab(k)=uitab(tabgroup,'Title', sprintf('Wf_%i', k));
-
- axes('parent',tab(k))
-
- plot(wfs{k});
-
- legend(sprintf('%i times', reps(k)));
-end
\ No newline at end of file
diff --git a/MATLAB/+qctoolkit/qctoolkitTestSetup.m b/MATLAB/+qctoolkit/qctoolkitTestSetup.m
deleted file mode 100644
index 1ba8ab65d..000000000
--- a/MATLAB/+qctoolkit/qctoolkitTestSetup.m
+++ /dev/null
@@ -1,86 +0,0 @@
-%% Setup
-global plsdata
-plsdata = struct( ...
- 'path', 'Y:\Cerfontaine\Code\qc-tookit-pulses', ...
- 'awg', struct('inst', [], 'hardwareSetup', []), ...
- 'daq', struct('inst', []) ...
- );
-
-
-%% Repetition 4 Channel charge pulse
-% Wwritten by F. Wangelik and P. Cerfontaine, 23.02.2018
-
-
-% Define table template for each iteration/step/measurement
-part_pulse = py.qctoolkit.pulses.TablePT( ...
- struct( ...
- 'W', py.list({ {'(t_wait+t_meas)/sample_rate', 'W_fast*(x_start + i_x*x_step) + W_slow*(y_start + i_y*y_step)'} }), ...
- 'X', py.list({ {'(t_wait+t_meas)/sample_rate', 'X_fast*(x_start + i_x*x_step) + X_slow*(y_start + i_y*y_step)'} }), ...
- 'Y', py.list({ {'(t_wait+t_meas)/sample_rate', 'Y_fast*(x_start + i_x*x_step) + Y_slow*(y_start + i_y*y_step)'} }), ...
- 'Z', py.list({ {'(t_wait+t_meas)/sample_rate', 'Z_fast*(x_start + i_x*x_step) + Z_slow*(y_start + i_y*y_step)'} }), ...
- 'marker', py.list({ {'(t_wait+t_meas)/sample_rate', 1} }) ...
- ) ...
- );
-
-first_pulse = py.qctoolkit.pulses.TablePT( ...
- pyargs( ...
- 'entries', ...
- struct( ...
- 'W', py.list({ {'(t_wait+t_meas)/sample_rate', 'W_fast*(x_start + i_x*x_step) + W_slow*(y_start + i_y*y_step)'} }), ...
- 'X', py.list({ {'(t_wait+t_meas)/sample_rate', 'X_fast*(x_start + i_x*x_step) + X_slow*(y_start + i_y*y_step)'} }), ...
- 'Y', py.list({ {'(t_wait+t_meas)/sample_rate', 'Y_fast*(x_start + i_x*x_step) + Y_slow*(y_start + i_y*y_step)'} }), ...
- 'Z', py.list({ {'(t_wait+t_meas)/sample_rate', 'Z_fast*(x_start + i_x*x_step) + Z_slow*(y_start + i_y*y_step)'} }), ...
- 'marker', py.list({ {'(t_wait+t_meas)/sample_rate', 1} }) ...
- ), ...
- 'measurements', ...
- py.list({ {'A', 't_wait/sample_rate', '(t_meas/sample_rate)*meas_time_multiplier'}, ...
- {'B', 't_wait/sample_rate', '(t_meas/sample_rate)*meas_time_multiplier'} } ...
- ) ...
- ) ...
- );
-
-rep_pulse = py.qctoolkit.pulses.repetition_pulse_template.RepetitionPulseTemplate(part_pulse, 'meas_time_multiplier-1');
-meas_pulse = py.qctoolkit.pulses.SequencePT(first_pulse, rep_pulse);
-
-% Create loop templates for both iterations
-x_loop = py.qctoolkit.pulses.loop_pulse_template.ForLoopPulseTemplate(meas_pulse, 'i_x', 'N_x');
-y_loop = py.qctoolkit.pulses.loop_pulse_template.ForLoopPulseTemplate(x_loop, 'i_y', 'N_y');
-
-% Start parameter mapping via mapping template
-general_charge_scan = py.qctoolkit.pulses.MappingPT( ...
- pyargs( ...
- 'template', y_loop, ...
- 'identifier', 'charge_scan', ...
- 'parameter_mapping', struct('x_step', '(x_stop-x_start)/N_x', ...
- 'y_step', '(y_stop-y_start)/N_y'), ...
- 'allow_partial_parameter_mapping', true ...
- ) ...
- );
-
-general_charge_scan = py.qctoolkit.pulses.repetition_pulse_template.RepetitionPulseTemplate( ...
- pyargs( ...
- 'body', general_charge_scan, ...
- 'repetition_count', 'rep_count', ...
- 'identifier', 'general_charge_scan' ...
- ) ...
- );
-
-%%
-qctoolkit.plot_pulse(general_charge_scan, struct('x_start', -1, 'x_stop', 1, 'N_x', 10, 't_meas', 1, ...
- 'W_fast', 1, 'W_slow', 0, ...
- 'X_fast', 1, 'X_slow', 0, ...
- 'Y_fast', 0, 'Y_slow', 1, ...
- 'Z_fast', 0, 'Z_slow', 1, ...
- 'y_start', -1, 'y_stop', 1, 'N_y', 10, 't_wait', 0, 'sample_rate', 2.3, 'meas_time_multiplier', 2, ...
- 'rep_count', 2), 100)
-
-
-% from qctoolkit.serialization import FilesystemBackend, Serializer
-
-%%
-backend = py.qctoolkit.serialization.FilesystemBackend(plsdata.path);
-serializer = py.qctoolkit.serialization.Serializer(backend);
-
-
-serializer.serialize(pyargs('serializable', general_charge_scan, 'overwrite', true))
-
diff --git a/README.md b/README.md
index 38509803a..317fb38be 100644
--- a/README.md
+++ b/README.md
@@ -23,10 +23,13 @@ The current feature list is as follows:
- Hardware model representation
- High-level pulse to hardware configuration and waveform translation routines
- Hardware drivers for Tabor Electronics, Tektronix and Zurich Instruments AWGs and AlazarTech Digitizers
-- MATLAB interface to access qupulse functionality
Pending changes are tracked in the `changes.d` subdirectory and published in [`RELEASE_NOTES.rst`](RELEASE_NOTES.rst) on release using the tool `towncrier`.
+### Removed features
+
+The previous name of this package was qctoolkit. It was renamed in 2017 to highlight the pulse focus. The backward compatible alias was removed after the 0.9 release. Furthermore, this repository had a MATLAB interface for a longer time which was removed at the same time.
+
## Installation
qupulse is available on [PyPi](https://pypi.org/project/qupulse/) and the latest release can be installed by executing:
```sh
@@ -38,28 +41,42 @@ Alternatively, the current development version of qupulse can be installed by ex
```sh
python -m pip install -e git+https://github.com/qutech/qupulse.git#egg=qupulse[default]
```
-which will clone the github repository to `./src/qupulse` and do an editable/development install.
+which will clone the github repository to `./src/qupulse` and do an editable/development install.
### Requirements and dependencies
-qupulse requires at least Python 3.7 and is tested on 3.7, 3.8 and 3.9. It relies on some external Python packages as dependencies.
+qupulse requires at least Python 3.10 and is tested on 3.10, 3.11 and 3.12. It relies on some external Python packages as dependencies.
We intentionally did not restrict versions of dependencies in the install scripts to not unnecessarily prevent usage of newer releases of dependencies that might be compatible. However, if qupulse does encounter problems with a particular dependency version please file an issue.
-The backend for TaborAWGs requires packages that can be found [here](https://git.rwth-aachen.de/qutech/python-TaborDriver). As a shortcut you can install it from the python interpreter via `qupulse.hardware.awgs.install_requirements('tabor')`.
+The backend for TaborAWGs requires packages that can be found [here](https://git.rwth-aachen.de/qutech/python-TaborDriver).
The data acquisition backend for AlazarTech cards needs a package that unfortunately is not open source (yet). If you need it or have questions contact .
## Documentation
-You can find documentation on how to use this library on [readthedocs](https://qupulse.readthedocs.io/en/latest/) and [IPython notebooks with examples in this repo](doc/source/examples). You can build it locally with `python setup.py build_sphinx`.
+You can find documentation on how to use this library on [readthedocs](https://qupulse.readthedocs.io/en/latest/) and [IPython notebooks with examples in this repo](doc/source/examples). You can build it locally with `hatch run docs:html` if you have pandoc installed.
### Folder Structure
-The repository primarily consists of the folders `qupulse` (toolkit core code) and `tests` (toolkit core tests). Additional parts of the project reside in `MATLAB` (MATLAB interface) and `doc` (configuration and source files to build documentation)
+The repository primarily consists of the folders `qupulse` (source code), `tests` and `doc`.
-`qupulse` contains the entire Python source code of the project and is further partitioned the following packages of related modules
+`qupulse` contains the entire Python source code of the project and is further partitioned the following packages of related packages
- `pulses` which contains all modules related to pulse representation.
- `hardware` containing classes for hardware representation as well as hardware drivers
- `utils` containing miscellaneous utility modules or wrapping code for external libraries
-- `_program` contains general and hardware specific representations of instantiated (parameter free) pulses. It is private because there is no stability guarantee.
+- `program` contains general and hardware specific representations of instantiated (parameter free) pulses.
+- `expression` contains the expression interface used by qupulse.
Contents of `tests` mirror the structure of `qupulse`. For every `` somewhere in `qupulse` there should exist a `Tests.py` in the corresponding subdirectory of `tests`.
+## Development
+
+`qupulse` uses `hatch` as development tool which provides a convenient interface for most development tasks. The following should work.
+
+ - `hatch build`: Build wheel and source tarball
+ - `hatch version X.X.X`: Set version
+ - `hatch run docs:html`: Build documentation (requires pandoc)
+ - `hatch run docs:clean-notebooks` to execute all example notebooks that start with 00-03 and clean all metadata.
+ - `hatch run changelog:draft` and `hatch run changelog:release` to preview or update the changelog.
+
+## License
+
+The current version of qupulse is available under the `LGPL-3.0-or-later` license. Versions up to and including 0.10 were licensed under the MIT license. If you require different licensing terms, please contact us to discuss your needs.
diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst
index c61c8b8c0..c76c7e9c9 100644
--- a/RELEASE_NOTES.rst
+++ b/RELEASE_NOTES.rst
@@ -2,6 +2,69 @@
.. towncrier release notes start
+qupulse 0.10 (2024-04-04)
+=========================
+
+Features
+--------
+
+- Move HDAWG driver to qupulse-hdawg-legacy to disentangle driver version from qupulse version. The new HDAWG driver will be published under qupulse-hdawg. (`#779 `_)
+- Add the `ProgramBuilder` interface pattern to make the generated program of `PulseTemplate.create_program` easily customizable. (`#781 `_)
+- Measurement windows can now automatically shrank in case of overlap to counteract small numeric errors. (`#791 `_)
+
+
+Bugfixes
+--------
+
+- ``ConstantPulseTemplate``s from all versions can now be deserialized. (`#696 `_)
+- Fixed that single segment tables where always interpreted to be constant. (`#707 `_)
+- Add missing pulse registry support to `ArithmeticPT`. (`#775 `_)
+
+
+qupulse 0.9 (2023-11-08)
+========================
+
+Features
+--------
+
+- Add `__pow__` as a repetition shortcut. This means you can do `my_pulse_template ** 5` or `my_pulse_template ** 'my_repetition_count'`. (`#692 `_)
+- Promote ``qupulse.expression`` to a subpackage and create ``qupulse.expression.protocol`` with protocol classes that define the expression interface that is supposed to be used by qupulse.
+ The ```sympy`` based implementation is moved to ``qupulse.expressions.sympy`` and imported in ``qupulse.expressions``.
+
+ The intended use is to be able to use less powerful but faster implementations of the ``Expression`` protocol where appropriate.
+ In this first iteration, qupulse still relies on internals of the ``sympy`` based implementation in many places which is to be removed in the future. (`#750 `_)
+- Promote parts of the private subpackage `qupulse._program` to the public subpackage `qupulse.program`, i.e. `loop`, `volatile`, `transformation` and `waveforms`. This allows external packages/drivers to rely on stability of the `Loop` class. (`#779 `_)
+- Add ``PulseTemplate.pad_to`` method to help padding to minimal lengths or multiples of given durations. (`#801 `_)
+
+
+Misc
+----
+
+- `#771 `_
+
+
+qupulse 0.8 (2023-03-28)
+========================
+
+Features
+--------
+
+- New two dimensional plotting function ``qupulse.pulses.plotting.plot_2d``. (`#703 `_)
+- Add support for time dependent expressions for arithmetics with atomic pulse templates i.e. ``ParallelChannelPT`` and
+ ``ArithmeticPT`` support time dependent expressions if used with atomic pulse templates.
+ Rename ``ParallelConstantChannelPT`` to ``ParallelChannelPT`` to reflect this change. (`#709 `_)
+- Add ``with_`` family of helper methods to ``PulseTemplate`` to allow convinient and easily discoverable pulse template
+ combination. (`#710 `_)
+- The plotting module is now located at `qupulse.plotting`. There is a legacy alias at `qupulse.pulses.plotting`. (`#735 `_)
+
+
+Deprecations and Removals
+-------------------------
+
+- Remove the ``Parameter``, ``MappedParameter`` and ``ConstantParameter`` classes that where deprecated in version 0.5. (`#512 `_)
+- Drop support for python version 3.7. (`#760 `_)
+
+
qupulse 0.7 (2022-10-05)
========================
diff --git a/changes.d/512.removal b/changes.d/512.removal
deleted file mode 100644
index 83a9d441b..000000000
--- a/changes.d/512.removal
+++ /dev/null
@@ -1 +0,0 @@
-Remove the `Parameter`, `MappedParameter` and `ConstantParameter` class that where deprecated in version 0.5.
diff --git a/changes.d/696.fix b/changes.d/696.fix
deleted file mode 100644
index 9df69bd0b..000000000
--- a/changes.d/696.fix
+++ /dev/null
@@ -1 +0,0 @@
-`ConstantPulseTemplate`s from all versions can now be deserialized.
\ No newline at end of file
diff --git a/changes.d/707.fix b/changes.d/707.fix
deleted file mode 100644
index 33e9e9f8e..000000000
--- a/changes.d/707.fix
+++ /dev/null
@@ -1 +0,0 @@
-Fixed that single segment tables where always interpreted to be constant.
diff --git a/changes.d/709.feature b/changes.d/709.feature
deleted file mode 100644
index 9bdf45b12..000000000
--- a/changes.d/709.feature
+++ /dev/null
@@ -1,3 +0,0 @@
-Add support for time dependent expressions for arithmetics with atomic pulse templates i.e. ParallelChannelPT and
-ArithmeticPT support time dependent expressions if used with atomic pulse templates.
-Rename `ParallelConstantChannelPT` to `ParallelChannelPT` to reflect this change.
diff --git a/changes.d/710.feature b/changes.d/710.feature
deleted file mode 100644
index 650482cc2..000000000
--- a/changes.d/710.feature
+++ /dev/null
@@ -1,2 +0,0 @@
-Add `with_` family of helper methods to `PulseTemplate` to allow convinient and easily discoverable pulse template
-combination.
diff --git a/changes.d/808.doc b/changes.d/808.doc
new file mode 100644
index 000000000..a27837ceb
--- /dev/null
+++ b/changes.d/808.doc
@@ -0,0 +1 @@
+Add an example with a Zurich Instruments HDAWG and MFLI.
\ No newline at end of file
diff --git a/changes.d/835.removal b/changes.d/835.removal
new file mode 100644
index 000000000..cc2f7a5f0
--- /dev/null
+++ b/changes.d/835.removal
@@ -0,0 +1 @@
+Remove python 3.8 and 3.9 support. Version 3.10 is now the minimal supported version.
\ No newline at end of file
diff --git a/changes.d/841.removal b/changes.d/841.removal
new file mode 100644
index 000000000..fe39edbe9
--- /dev/null
+++ b/changes.d/841.removal
@@ -0,0 +1 @@
+Remove MATLAB code and the qctoolkit alias for qupulse.
diff --git a/changes.d/845.removal b/changes.d/845.removal
new file mode 100644
index 000000000..b47bfa1fb
--- /dev/null
+++ b/changes.d/845.removal
@@ -0,0 +1 @@
+Fallback for a missing `gmpy2` via `fractions` was removed.
\ No newline at end of file
diff --git a/changes.d/853.misc b/changes.d/853.misc
new file mode 100644
index 000000000..cf8b7ff32
--- /dev/null
+++ b/changes.d/853.misc
@@ -0,0 +1 @@
+Remove private and unused frozendict fallback implementations `_FrozenDictByInheritance` and `_FrozenDictByWrapping`.
\ No newline at end of file
diff --git a/changes.d/882.feature b/changes.d/882.feature
new file mode 100644
index 000000000..008540475
--- /dev/null
+++ b/changes.d/882.feature
@@ -0,0 +1 @@
+Add functions ``PulseTemplate.pad_selected_subtemplates_to`` for padding inner templates and ``PulseTemplate.with_mapped_subtemplates`` as implementation helper.
diff --git a/changes.d/882.removal b/changes.d/882.removal
new file mode 100644
index 000000000..136b2d3b6
--- /dev/null
+++ b/changes.d/882.removal
@@ -0,0 +1 @@
+The ``pt_kwargs`` keyword argument of ``PulseTemplate.pad_to`` is now deprecated and was replaced by the ``spt_kwargs`` argument which no longer enforces sequence pulse template creation even if no padding is required.
diff --git a/changes.d/888.removal b/changes.d/888.removal
new file mode 100644
index 000000000..fcb30d9d4
--- /dev/null
+++ b/changes.d/888.removal
@@ -0,0 +1 @@
+Remove unused `qupulse.comparable` module.
diff --git a/changes.d/904.removal b/changes.d/904.removal
new file mode 100644
index 000000000..2ab5c7c0c
--- /dev/null
+++ b/changes.d/904.removal
@@ -0,0 +1 @@
+Remove long deprecated `AtomicPulseTemplate.atomicity`.
diff --git a/changes.d/906.feature b/changes.d/906.feature
new file mode 100644
index 000000000..57969486b
--- /dev/null
+++ b/changes.d/906.feature
@@ -0,0 +1,4 @@
+Add metadata attribute to ``PulseTemplate`` and the keyword argument to ``SequencePT``, ``RepetitionPT``, ``ForLoopPT`` and ``MappingPT``.
+The metadata is intended for user data that does not influence the pulse itself. It is serialized with the pulse template but not part of the equality check because the field is mutable.
+
+Currently, the only field that is used by qupulse itself is ``to_single_waveform``. When ``to_single_waveform='always'`` is set kin the metadata the corresponding pulse template is translated into a single waveform on program creation.
diff --git a/changes.d/907.removal b/changes.d/907.removal
new file mode 100644
index 000000000..f0c5bedbd
--- /dev/null
+++ b/changes.d/907.removal
@@ -0,0 +1,2 @@
+The internal structure of `qupulse.program` changed. `Program` and `ProgramBuilder` moved to `qupulse.program.protocol`
+with a backwards compatible import. `SimpleExpression` was renamed to `DynamicLinearValue` and lives now in `qupulse.program.values`.
diff --git a/changes.d/933.feature b/changes.d/933.feature
new file mode 100644
index 000000000..2f44e23f3
--- /dev/null
+++ b/changes.d/933.feature
@@ -0,0 +1 @@
+Replace PulseTemplate._create_program(**a_lot_of__kwargs) with PulseTemplate._build_program(program_builder).
\ No newline at end of file
diff --git a/coverage.ini b/coverage.ini
index f92469606..aeac2c624 100644
--- a/coverage.ini
+++ b/coverage.ini
@@ -3,4 +3,3 @@ branch = True
omit =
*main.py
*__init__.py
- */qcmatlab/manager.py
diff --git a/doc/Makefile b/doc/Makefile
deleted file mode 100644
index d0c3cbf10..000000000
--- a/doc/Makefile
+++ /dev/null
@@ -1,20 +0,0 @@
-# Minimal makefile for Sphinx documentation
-#
-
-# You can set these variables from the command line, and also
-# from the environment for the first two.
-SPHINXOPTS ?=
-SPHINXBUILD ?= sphinx-build
-SOURCEDIR = source
-BUILDDIR = build
-
-# Put it first so that "make" without argument is like "make help".
-help:
- @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
-
-.PHONY: help Makefile
-
-# Catch-all target: route all unknown targets to Sphinx using the new
-# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
-%: Makefile
- @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/doc/README.md b/doc/README.md
index 4aab70678..014783155 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -8,6 +8,6 @@ You may either build the documentation yourself or read it on [readthedocs](http
In the subdirectory *examples* you can find various [Jupyter notebook](http://jupyter.org/) files providing some step-by-step examples of how qupulse can be used. These can be explored in an interactive fashion by running the *Jupyter notebook* application inside the folder. However, a static version will also be included in the documentation created with *sphinx*.
## Building the Documentation
-To build the documentation, you will need [sphinx](http://www.sphinx-doc.org/en/stable/) and [nbsphinx](https://nbsphinx.readthedocs.org/) which, in turn, requires [pandoc](http://pandoc.org/).
+To build the documentation, you will need [sphinx](http://www.sphinx-doc.org/en/stable/) and [nbsphinx](https://nbsphinx.readthedocs.org/) which, in turn, requires [pandoc](http://pandoc.org/) which must be installed separately.
-The documentation is built by invoking `make ` inside the */doc* directory, where `` is an output format supported by *sphinx*, e.g., `html`. The output will then be found in `/doc/build/`.
+You can use hatch to build the documentation locally via `hatch run docs:build ` or a bit more concise `hatch run docs:html`. The output will then be found in `/doc/build/`.
diff --git a/doc/make.bat b/doc/make.bat
deleted file mode 100644
index 9534b0181..000000000
--- a/doc/make.bat
+++ /dev/null
@@ -1,35 +0,0 @@
-@ECHO OFF
-
-pushd %~dp0
-
-REM Command file for Sphinx documentation
-
-if "%SPHINXBUILD%" == "" (
- set SPHINXBUILD=sphinx-build
-)
-set SOURCEDIR=source
-set BUILDDIR=build
-
-if "%1" == "" goto help
-
-%SPHINXBUILD% >NUL 2>NUL
-if errorlevel 9009 (
- echo.
- echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
- echo.installed, then set the SPHINXBUILD environment variable to point
- echo.to the full path of the 'sphinx-build' executable. Alternatively you
- echo.may add the Sphinx directory to PATH.
- echo.
- echo.If you don't have Sphinx installed, grab it from
- echo.http://sphinx-doc.org/
- exit /b 1
-)
-
-%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
-goto end
-
-:help
-%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
-
-:end
-popd
diff --git a/doc/requirements.txt b/doc/requirements.txt
deleted file mode 100644
index 575c546ab..000000000
--- a/doc/requirements.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-sphinx==4.4.0
-nbsphinx==0.8.8
-ipykernel==6.9.1
diff --git a/doc/source/_templates/autosummary/package.rst b/doc/source/_templates/autosummary/package.rst
index aa58817da..ebe119f62 100644
--- a/doc/source/_templates/autosummary/package.rst
+++ b/doc/source/_templates/autosummary/package.rst
@@ -8,7 +8,7 @@
:toctree:
:recursive:
{% for item in modules %}
- {{ item }}
+ {{ fullname }}.{{ item }}
{%- endfor %}
{% endif %}
{% endblock %}
diff --git a/doc/source/concepts/awgs.rst b/doc/source/concepts/awgs.rst
new file mode 100644
index 000000000..e51fc45a0
--- /dev/null
+++ b/doc/source/concepts/awgs.rst
@@ -0,0 +1,33 @@
+.. _awgs:
+
+How qupulse models AWGs
+-----------------------
+
+This section is supposed to help you understand how qupulse sees AWGs and by extension help you understand the driver implementations in :py:mod:`~qupulse.hardware.awgs` and :py:mod:`~qupulse.hardware.feature_awg`.
+
+When a program is uploaded to an arbitrary waveform generator (AWG) it needs to brought in a form that the hardware
+understands.
+Most AWGs consist of three significant parts:
+
+* The actual digital to analog converter (DAC) that outputs samples at a (semi-) fixed rate [1]_,
+* a sequencer which tells the DAC what to do,
+* waveform memory which contains sampled waveforms in a format that the DAC understands.
+
+The sequencer feeds the data from the waveform memory to the DAC in the correct order.
+Uploading a qupulse pulse to an AWG requires to sample the program, upload waveforms to the memory
+and program the sequencer.
+
+The interface exposed by the vendor to program the sequencer reaches from a simple table like for
+Tektronix' AWG5000 series to some kind of complex domain specific language (DSL) like Zurich Instrument' sequencing C.
+
+Basically all AWGs have some kind of limitations regarding the length of the waveform samples which is often of the
+form :math:`n_{\texttt{samples}} = n_{\texttt{min}} + m \cdot n_{\texttt{div}}` with the minimal number of samples
+:math:`n_{\texttt{min}}` and some divisor :math:`n_{\texttt{div}}`.
+
+.. topic:: Implementation detail (might be outdated)
+
+ Holding a voltage for a long time was often best accomplished by repeating a waveform of :math:`n_{\texttt{min}}` to save waveform memory.
+ Earlier versions of qupulse required you to write your pulse in this way i.e. with a ``RepetitionPT``.
+ Now qupulse contains the function ``qupulse._program._loop.roll_constant_waveform`` which detects long constant waveforms and rolls them into corresponding repetitions. This should be done by the hardware backend automatically.
+
+.. [1] Some AWGs like the HDAWG can be programmed change the sample rate to a divisor of the "main" rate dynamically.
diff --git a/doc/source/concepts/concepts.rst b/doc/source/concepts/concepts.rst
index ac17a2c44..a191bb7e1 100644
--- a/doc/source/concepts/concepts.rst
+++ b/doc/source/concepts/concepts.rst
@@ -6,4 +6,7 @@ This section explains the fundamental design concepts of qupulse.
.. toctree::
pulsetemplates
serialization
- instantiating
\ No newline at end of file
+ instantiating
+ program
+ awgs
+
diff --git a/doc/source/concepts/instantiating.rst b/doc/source/concepts/instantiating.rst
index e999e3b51..387db5db6 100644
--- a/doc/source/concepts/instantiating.rst
+++ b/doc/source/concepts/instantiating.rst
@@ -7,19 +7,25 @@ As already briefly mentioned in :ref:`pulsetemplates`, instantiation of pulses i
interpretable representation of a concrete pulse ready for execution from the quite high-level :class:`.PulseTemplate`
object tree structure that defines parameterizable pulses in qupulse.
-This is a two-step process that involves
+The entry point is the :meth:`.PulseTemplate.create_program` method of the :class:`.PulseTemplate` hierarchy.
+It accepts the pulse parameters, and allows to rename and/or omit channels or measurements.
+It checks that the provided parameters and mappings are consistent and meet the optionally defined parameter constraints of the pulse template.
+The translation target is defined by the :class:`.ProgramBuilder` argument.
-#. Inserting concrete parameter values and obtaining a hardware-independent pulse program tree
-#. Flattening that tree, sampling and merging of leaf waveforms according to needs of hardware
+Each pulse template knows what program builder methods to call to translate itself.
+For example, the :class:`.ConstantPulseTemplate` calls :meth:`.ProgramBuilder.hold_voltage` to hold a constant voltage for a defined amount of time while the :class:`.SequncePulseTemplate` forwards the program builder to the sub-templates in order.
+The resulting program is completely backend dependent.
-This separation allows the first step to be performed in a hardware-agnostic way while the second step does not have
-to deal with general functionality and can focus only on hardware-specific tasks. Step 1 is implemented in the
-:meth:`.PulseTemplate.create_program` method of the :class:`.PulseTemplate` hierarchy. It checks parameter consistency
-with parameter constraints and returns an object of type
-:class:`.Loop` which represents a pulse as nested loops of atomic waveforms. This is another object tree structure
-but all parameters (including repetition counts) have been substituted by the corresponding numeric values passed into
-``create_program``. The :class:`.Loop` object acts as your reference to the instantiated pulse.
-See :ref:`/examples/06CreatePrograms.ipynb` for an example on usage of :meth:`.PulseTemplate.create_program`.
+**Historically**, there was only a single program type :class:`.Loop` which is still the default output type.
+As the time of this writing there is the additional :class:`.LinSpaceProgram` which allows for the efficient representation of linearly spaced voltage changes in arbitrary control structures. There is no established way to handle the latter yet.
+The following describes handling of :class:`.Loop` object only via the :class:`qupulse.hardware.HardwareSetup`.
+
+The :class:`.Loop` class was designed as a hardware-independent pulse program tree for waveform table based sequencers.
+Therefore, the translation into a hardware specific format is a two-step process which consists of the loop object creation as a first step
+and the transformation of that tree according to the needs of the hardware as a second step.
+However, the AWGs became more flexibly programmable over the years as discussed in :ref:`awgs`.
+
+The first step of this pulse instantiation is showcased in :ref:`/examples/02CreatePrograms.ipynb` where :meth:`.PulseTemplate.create_program` is used to create a :class:`.Loop` program.
The second step of the instantiation is performed by the hardware backend and transparent to the user. Upon registering
the pulse with the hardware backend via :meth:`qupulse.hardware.HardwareSetup.register_program`, the backend will determine which
@@ -37,6 +43,6 @@ by the driver with its neighbors in the execution sequence until the minimum wav
optimizations and merges (or splits) of waveforms for performance are also possible.
In contrast, the Zurich Instruments HDAWG allows arbitrary nesting levels and is only limited by the instruction cache.
+However, this device supports increment commands which allow the efficient representation of linear voltage sweeps which is **not** possible with the :class:`.Loop` class.
-However, as already mentioned, the user does not have to be concerned about this in regular use of qupulse, since this
-is dealt with transparently in the hardware backend.
+The section :ref:`program` touches the ideas behind the current program implementations i.e. :class:`.Loop` and :class:`.LinSpaceProgram`.
diff --git a/doc/source/concepts/program.rst b/doc/source/concepts/program.rst
new file mode 100644
index 000000000..8591309bd
--- /dev/null
+++ b/doc/source/concepts/program.rst
@@ -0,0 +1,34 @@
+.. _program:
+
+Instantiated Pulse: Program
+---------------------------
+
+In qupulse an instantiated pulse template is called a program as it is something that an arbitrary waveform generator (AWG) can execute/playback.
+It can be thought of as compact representation of a mapping :math:`\{t | 0 \le t \le t_{\texttt{duration}}\} \rightarrow \mathbb{R}^n` from the time while the program lasts :math:`t` to an n-dimensional voltage space :math:`\mathbb{R}^n`.
+The dimensions are named by the channel names.
+
+Programs are created by the :meth:`~.PulseTemplate.create_program` method of `PulseTemplate` which returns a hardware independent and un-parameterized representation.
+The method takes a ``program_builder`` keyword argument that is propagated through the pulse template tree and thereby implements the visitor pattern.
+If the argument is not passed :func:`~qupulse.program.default_program_builder()` is used instead which is :class:`.LoopBuilder` by default, i.e. the program created by default is of type :class:`.Loop`. The available program builders, programs and their constituents like :class:`.Waveform` and :class:`.VolatileRepetitionCount` are defined in th :mod:`qupulse.program` subpackage and it's submodules. There is a private ``qupulse._program`` subpackage that was used for more rapid iteration development and is slowly phased out. It still contains the hardware specific program representation for the tabor electronics AWG driver. Zurich instrument specific code has been factored into the separate package ``qupulse-hdawg``. Please refer to the reference and the docstrings for exact interfaces and implementation details.
+
+The :class:`.Loop` default program is the root node of a tree of loop objects of arbitrary depth.
+Each node consists of a repetition count and either a waveform or a sequence of nodes which are repeated that many times.
+Iterations like the :class:`.ForLoopPT` cannot be represented natively but are unrolled into a sequence of items.
+The repetition count is currently the only property of a program that can be defined as volatile. This means that the AWG driver tries to upload the program in a way, where the repetition count can quickly be changed. This is implemented via the ``VolatileRepetitionCount`` class.
+
+A much more capable program format is :class:`.LinSpaceNode` which efficiently encodes linearly spaced sweeps in voltage space by utilizing increment commands. It is build via :class:`.LinSpaceBuilder`.
+The main complexity of this program class is the efficient handling of interleaved constant points.
+The increment and set commands do not only carry a channel and a value but also a dependency key which encodes the dependence of loop indices.
+This allows the efficient encoding of
+
+.. code:: python
+
+ for idx in range(10):
+ set_voltage(CONSTANT) # No dependencies
+ set_voltage(OFFSET + idx * FACTOR) # depends on idx with
+
+ for _ in range(10): # loop
+ set_voltage(CONSTANT, key=None)
+ increment_by(FACTOR, key=(FACTOR,))
+
+The motivation is that increment commands with this capability are available in the HDAWG command table.
diff --git a/doc/source/concepts/pulsetemplates.rst b/doc/source/concepts/pulsetemplates.rst
index 4b84d3fbc..a94ec578b 100644
--- a/doc/source/concepts/pulsetemplates.rst
+++ b/doc/source/concepts/pulsetemplates.rst
@@ -8,7 +8,7 @@ qupulse represents pulses as abstract pulse templates. A pulse template can be u
There are multiple types of different pulse template classes, briefly explained in the following.
:class:`.TablePulseTemplate`, :class:`.PointPulseTemplate` and :class:`.FunctionPulseTemplate` are used to define the atomic building blocks of pulses in the following ways: :class:`.TablePulseTemplate` and :class:`.PointPulseTemplate` allow the user to specify pairs of time and voltage values and choose an interpolation strategy between neighbouring points. Both templates support multiple channels but :class:`.TablePulseTemplate` allows for different time values for different channels meaning that the channels can change their voltages at different times. :class:`.PointPulseTemplate` restricts this to switches at the same time by interpreting the voltage as a vector and provides a more convenient interface for this case.
-:class:`.FunctionPulseTemplate` accepts any mathematical function that maps time to voltage values. Internally it uses :class:`.Expression` for function evaluation.
+:class:`.FunctionPulseTemplate` accepts any mathematical function that maps time to voltage values. Internally it uses :class:`qupulse.expressions.Expression` for function evaluation.
All other pulse template classes are then used to construct arbitrarily complex pulse templates by combining existing ones into new structures [#tree]_:
:class:`.SequencePulseTemplate` enables the user to specify a sequence of existing pulse templates (subtemplates) and modify parameter values using a mapping function.
@@ -20,8 +20,6 @@ In some cases, it is desired to write a pulse which partly consists of placehold
You can do some simple arithmetic with pulses which is implemented via :class:`.ArithmeticPulseTemplate` and :class:`.ArithmeticAtomicPulseTemplate`. The relevant arithmetic operators are overloaded so you do not need to use these classes directly.
-In the future might be pulse templates that allow conditional execution like a `BranchPulseTemplate` or a `WhileLoopPulseTemplate`.
-
All of these pulse template variants can be similarly accessed through the common interface declared by the :class:`.PulseTemplate` base class. [#pattern]_
As the class names are quite long the recommended way for abbreviation is to use the aliases defined in :py:mod:`~qupulse.pulses`. For example :class:`.FunctionPulseTemplate` is aliased as :class:`.FunctionPT`
@@ -33,7 +31,7 @@ Parameters
As mentioned above, all pulse templates may depend on parameters. During pulse template initialization the parameters simply are the free variables of expressions that occur in the pulse template. For example the :class:`.FunctionPulseTemplate` has expressions for its duration and the voltage time dependency i.e. the underlying function. Some pulse templates provided means to constrain parameters by accepting a list of :class:`.ParameterConstraint` which encapsulate comparative expressions that must evaluate to true for a given parameter set to successfully instantiate a pulse from the pulse template. This can be used to encode physical or logical parameter boundaries at pulse level.
-The mathematical expressions (for parameter transformation or as the function of the :class:`.FunctionPulseTemplate`) are encapsulated into an :class:`.Expression` class which wraps `sympy `_ for string evaluation.
+The mathematical expressions (for parameter transformation or as the function of the :class:`.FunctionPulseTemplate`) are encapsulated into an :class:`.sympy.Expression` class which wraps `sympy `_ for string evaluation by default. Other more performant or secure backends can potentially be implemented by conforming to the :class:`.protocol.Expression`.
Parameters can be mapped to arbitrary expressions via :class:`.mapping_pulse_template.MappingPulseTemplate`. One use case can be deriving pulse parameters from physical quantities.
@@ -45,6 +43,62 @@ Measurements
Pulses are usually used to manipulate the state of some physical system and the system's response has to be somehow validated and thus measured. qupulse pulse templates allow to define measurement windows that specify at what times measurements should be made and identify those windows with an identifier.
After the pulse templates are instantiated, uploading the resulting pulses to the hardware setup will cause qupulse to also configure corresponding measurement devices according to the specified measurement windows.
+Metadata
+^^^^^^^^
+
+Pulse templates have a :attr:`.PulseTemplate.metadata` attribute which is the only mutable part of the class. It is the place for translation hints and user-data. Currently, the only effect on qupulse is that all pulse templates that have the metadata field :attr:`.TemplateMetadata.to_single_waveform` set to 'always' are translated into a single waveform.
+
+Since it is a regular python object, you can freely add any fields you like to it without changing the class. All your custom fields will be serialized by qupulse if you want to save the template to disk.
+
+Due to the non-constant nature of the metadata field it is ignored when comparing pulse templates. This is necessary, because pulse templates implement hash.
+
+Convenience Functionality
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+In addition to the fundamental concepts and subclassing structure, :class:`.PulseTemplate` offers a variety of
+**convenience features** that make it easier to combine and modify pulses:
+
+- **Convenience Methods**:
+
+ - :meth:`.PulseTemplate.with_parallel_channels` attaches additional constant channel values in parallel to the
+ existing channels of a pulse.
+ - :meth:`.PulseTemplate.with_repetition` repeats a pulse a specified number of times.
+ - :meth:`.PulseTemplate.with_mapping` enables mapping or renaming of parameters, channel names, and measurement
+ identifiers.
+ - :meth:`.PulseTemplate.with_iteration` wraps a pulse into a loop construct, allowing iteration over an index with a
+ defined range.
+ - :meth:`.PulseTemplate.with_time_reversal` generates a time-reversed version of a pulse.
+ - :meth:`.PulseTemplate.with_appended` concatenates additional pulse templates, creating a sequence which can be more
+ concise than manually building a :class:`.SequencePulseTemplate`.
+
+- **Padding**:
+
+ - :meth:`.PulseTemplate.pad_to` extends the duration of a pulse template to a desired length, automatically appending
+ a constant pulse segment that matches the final output values.
+ - :meth:`.PulseTemplate.pad_selected_subtemplates_to` extends the duration selected subtemplates to a desired length, automatically appending
+ a constant pulse segment that matches the final output values. By default it pads all atomic subtemplates.
+
+- **Properties**:
+
+ - :attr:`.PulseTemplate.duration` provides an expression for the pulse’s total duration.
+ - :attr:`.PulseTemplate.initial_values` and :attr:`.PulseTemplate.final_values` specify the voltage levels at the
+ start and end of the pulse, respectively.
+ - :attr:`.PulseTemplate.integral` returns an expression evaluating the area under the pulse for each channel.
+
+- **Arithmetic**:
+
+ - Basic arithmetic operators (``+``, ``-``, ``*``, ``/``) are overloaded to allow direct numerical combination or
+ scaling of pulses.
+ - The :meth:`@` operator (matrix multiplication) is repurposed to represent time-wise concatenation of pulses (see
+ :meth:`.SequencePulseTemplate.concatenate`).
+
+- **Utility**:
+
+ - :meth:`.PulseTemplate.with_mapped_subtemplates` allows mapping some or all subtemplates with a configurable recursion strategy. It is used to implement :meth:`.PulseTemplate.pad_selected_subtemplates_to`.
+
+These convenience features allow for more compact, readable, and flexible pulse definitions, helping to focus on the
+overall structure of the experiment.
+
Obtaining a Concrete Pulse (Pulse Instantiation)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -52,7 +106,7 @@ To obtain a pulse ready for execution on the hardware from a pulse template, the
In order to translate the object structures that encode the pulse template in the software into a (sequential) representation of the concrete pulse with the given parameter values that is understandable by the hardware, we proceed in several steps.
-First, the :meth:`.PulseTemplate.create_program` checks parameter consistency with parameter constraints and translates the pulse template into an instantiated program object, which is then further interpreted and sequenced by the hardware backend code (in :py:mod:`~qupulse.hardware`).
+First, the :meth:`.PulseTemplate.create_program` checks parameter consistency with parameter constraints and translates the pulse template into an instantiated program object. The nature of this program depends on the targeted hardware and is determined by the ``program_builder`` keyword argument. This program is further interpreted and sequenced by the hardware backend code (in :py:mod:`~qupulse.hardware`).
See :ref:`instantiating` for a more in-depth explanation of instantiating pulses.
@@ -62,20 +116,22 @@ Relevant Examples
Examples demonstrating the construction of pulse templates and parameters from very simple to somewhat more complex pulses are
* :ref:`/examples/00SimpleTablePulse.ipynb`
-* :ref:`/examples/01AdvancedTablePulse.ipynb`
-* :ref:`/examples/02FunctionPulse.ipynb`
-* :ref:`/examples/03PointPulse.ipynb`
-* :ref:`/examples/03xComposedPulses.ipynb`
-* :ref:`/examples/03ConstantPulseTemplate.ipynb`
-* :ref:`/examples/05MappingTemplate.ipynb`
-* :ref:`/examples/07MultiChannelTemplates.ipynb`
-* :ref:`/examples/14ArithmeticWithPulseTemplates.ipynb`
+* :ref:`/examples/00AdvancedTablePulse.ipynb`
+* :ref:`/examples/00FunctionPulse.ipynb`
+* :ref:`/examples/00PointPulse.ipynb`
+* :ref:`/examples/00ComposedPulses.ipynb`
+* :ref:`/examples/00ConstantPulseTemplate.ipynb`
+* :ref:`/examples/00MappingTemplate.ipynb`
+* :ref:`/examples/00MultiChannelTemplates.ipynb`
+* :ref:`/examples/00ArithmeticWithPulseTemplates.ipynb`
+
+:ref:`/examples/01ParameterConstraints.ipynb` demonstrates the mentioned parameter constraints.
-:ref:`/examples/09ParameterConstraints.ipynb` demonstrates the mentioned parameter constraints.
+:ref:`/examples/01Measurements.ipynb` shows how to specify measurements.
-:ref:`/examples/08Measurements.ipynb` shows how to specify measurements.
+:ref:`/examples/02CreatePrograms.ipynb` illustrates usage of the :meth:`.PulseTemplate.create_program` method.
-Finally, :ref:`/examples/06CreatePrograms.ipynb` illustrates usage of the :meth:`.PulseTemplate.create_program` method.
+:ref:`physical_examples` show realistic use cases of pulse templates.
.. rubric:: Footnotes
.. [#tree] Regarded as objects in the programming language, each pulse template is a tree of PulseTemplate objects, where the atomic templates (:class:`.TablePulseTemplate` and :class:`.FunctionPulseTemplate` objects) are the leafs while the remaining ones form the inner nodes of the tree.
diff --git a/doc/source/concepts/serialization.rst b/doc/source/concepts/serialization.rst
index 080de40d1..e44566ffe 100644
--- a/doc/source/concepts/serialization.rst
+++ b/doc/source/concepts/serialization.rst
@@ -14,7 +14,7 @@ The :class:`.PulseStorage` offers a convenient dictionary-like interface for sto
Finally, the :class:`.StorageBackend` interface abstracts the actual storage backend. While currently there only exists a few implementations of this interface, most importantly the :class:`.FilesystemStorageBackend`, this allows to support, e.g., database storage, in the future. :class:`.PulseStorage` requires an instance of :class:`.StorageBackend` which represents its persistent pulse storage during initialization.
-For an example of how to use :class:`.PulseStorage` to store and load pulse templates, see :ref:`/examples/04PulseStorage.ipynb` in the examples section.
+For an example of how to use :class:`.PulseStorage` to store and load pulse templates, see :ref:`/examples/01PulseStorage.ipynb` in the examples section.
Global Pulse Registry
^^^^^^^^^^^^^^^^^^^^^^
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 232f53c67..4d12f5e5c 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -63,7 +63,7 @@
autoclass_content = 'both'
autosummary_generate = True
-napoleon_include_init_with_doc = True
+napoleon_include_init_with_doc = False
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -81,7 +81,7 @@
# General information about the project.
project = 'qupulse'
-copyright = '2015-2022, Quantum Technology Group, RWTH Aachen University'
+copyright = '2015-2024, Quantum Technology Group, RWTH Aachen University'
author = 'Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University'
# The version info for the project you're documenting, acts as replacement for
@@ -98,7 +98,7 @@
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
-language = None
+language = 'en'
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
@@ -119,7 +119,10 @@
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
-#add_module_names = True
+add_module_names = False
+
+# A string that determines how domain objects (functions, classes, attributes, etc.) are displayed in their table of contents entry.
+toc_object_entries_show_parents = 'hide'
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
@@ -320,8 +323,9 @@
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
- 'python': ('https://docs.python.org/', None),
- 'numpy': ('http://docs.scipy.org/doc/numpy/', None)
+ 'python': ('https://docs.python.org/3/', None),
+ 'numpy': ('https://numpy.org/doc/stable/', None),
+ 'sympy': ('https://docs.sympy.org/latest/', None),
}
nbsphinx_execute_arguments = [
@@ -330,10 +334,10 @@
]
-def skip(app, what, name, obj, skip, options):
+def skip_init(app, what, name, obj, skip, options):
if name == "__init__" and hasattr(obj, '__doc__') and isinstance(obj.__doc__, str) and len(obj.__doc__):
return True
- return skip
+ return None
def change_property_rtype_to_type(app, what, name, obj, options, lines):
if what == 'attribute':
@@ -342,5 +346,5 @@ def change_property_rtype_to_type(app, what, name, obj, options, lines):
lines[i] = line.replace(':rtype: :py:class:', ':type:')
def setup(app):
- app.connect('autodoc-skip-member', skip)
+ app.connect('autodoc-skip-member', skip_init)
#app.connect('autodoc-process-docstring', change_property_rtype_to_type)
diff --git a/doc/source/examples/12AbstractPulseTemplate.ipynb b/doc/source/examples/00AbstractPulseTemplate.ipynb
similarity index 87%
rename from doc/source/examples/12AbstractPulseTemplate.ipynb
rename to doc/source/examples/00AbstractPulseTemplate.ipynb
index 050e81f91..7c2899b9e 100644
--- a/doc/source/examples/12AbstractPulseTemplate.ipynb
+++ b/doc/source/examples/00AbstractPulseTemplate.ipynb
@@ -14,9 +14,7 @@
{
"cell_type": "code",
"execution_count": 1,
- "metadata": {
- "scrolled": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"from qupulse.pulses import AbstractPT, FunctionPT, AtomicMultiChannelPT, PointPT\n",
@@ -46,7 +44,7 @@
"output_type": "stream",
"text": [
"The integral has been declared so we can get it\n",
- "{'Y': Expression('a*b + sin(t_manip)'), 'X': Expression('t_init - cos(t_manip) + 2')}\n",
+ "{'X': ExpressionScalar('t_init/2 - cos(t_manip) + 2'), 'Y': ExpressionScalar('a*b + t_init/2 + sin(t_manip)')}\n",
"\n",
"We get an error that for the pulse \"readout\" the property \"duration\" was not specified:\n",
"NotSpecifiedError('readout', 'duration')\n"
@@ -84,7 +82,7 @@
"text": [
"With wrong integral value:\n",
"RuntimeError('Cannot link to target. Wrong value of property \"integral\"')\n",
- "the linking worked. The new experiment has now a defined duration of Expression('t_init + t_manip + t_read') .\n"
+ "the linking worked. The new experiment has now a defined duration of ExpressionScalar('t_init + t_manip + t_read') .\n"
]
}
],
@@ -107,22 +105,8 @@
}
],
"metadata": {
- "kernelspec": {
- "display_name": "Python 3",
- "language": "python",
- "name": "python3"
- },
"language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.7.2"
+ "name": "python"
}
},
"nbformat": 4,
diff --git a/doc/source/examples/00AdvancedTablePulse.ipynb b/doc/source/examples/00AdvancedTablePulse.ipynb
new file mode 100644
index 000000000..1ea295c7b
--- /dev/null
+++ b/doc/source/examples/00AdvancedTablePulse.ipynb
@@ -0,0 +1,150 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Modelling an Advanced TablePulseTemplate\n",
+ "\n",
+ "[The SimpleTablePulse example](00SimpleTablePulse.ipynb) shows how a simple parametrized ```TablePT``` on one channel can implemented and how the interpolation works.\n",
+ "\n",
+ "This example demonstrates how to set up a more complex ```TablePT```. This means we will include multiple channels and use expressions for times and voltages.\n",
+ "\n",
+ "First lets reimplement the pulse from the previous example but this time with a second channel `'B'`, that has the same voltage values but negative. To do this, we extend the entry dict by a second item with the channel ID `'B'` as key and the entry list as value.\n",
+ "\n",
+ "Then we plot it to see that it actually works."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjUAAAGwCAYAAABRgJRuAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAA9K0lEQVR4nO3deXxM9+L/8fdkT2QhhCSEiAS1xK619FprvVq9La5bRHcudVG3GlXbvRJ0UcpPq6Voq6VVrVZLVVVL7UtR1LWrLdYkhAnJ/P7wNbe5EmZikpM5eT0fj3k8MvM5c+ad0SbvfM5nzrHYbDabAAAA3JyH0QEAAABcgVIDAABMgVIDAABMgVIDAABMgVIDAABMgVIDAABMgVIDAABMwcvoAIUpOztbJ06cUFBQkCwWi9FxAACAA2w2m9LT0xUZGSkPj7znY4pVqTlx4oSioqKMjgEAAPLh2LFjqlChQp7jxarUBAUFSbrxpgQHBxucBgAAOCItLU1RUVH23+N5KVal5uYhp+DgYEoNAABu5k5LR1goDAAATIFSAwAATIFSAwAATKFYrakBAJhHVlaWrl27ZnQMuIC3t7c8PT3vej+UGgCAW7HZbDp16pQuXrxodBS4UMmSJRUeHn5X55Gj1AAA3MrNQlO2bFkFBARwMlU3Z7PZlJGRoZSUFElSREREvvdFqQEAuI2srCx7oSldurTRceAi/v7+kqSUlBSVLVs234eiWCgMAHAbN9fQBAQEGJwErnbz3/Ru1klRagAAbodDTubjin9TSg0AADAFSg0AADAFSg0AAAY6fPiwLBaLtm/fbnQUh7Rs2VKDBw82OkauKDUAAMBl5syZI4vFYr8FBgaqQYMG+uyzzwr8tSk1AADApYKDg3Xy5EmdPHlS27ZtU/v27dW9e3f99ttvBfq6lBoAgFuz2WzKyLxe6DebzeZwxuzsbE2aNEmxsbHy9fVVxYoVNX78+BzbHDx4UK1atVJAQIDq1KmjdevW2cfOnTunnj17qnz58goICFDt2rX10Ucf5Xh+y5YtNWjQIL3wwgsKDQ1VeHi4xowZk2Mbi8Wid999Vw8//LACAgIUFxenJUuW5Nhm165d6tixowIDA1WuXDn17t1bZ8+edfh7vfk64eHhCg8PV1xcnP7973/Lw8NDO3bscGo/zuLkewAAt3blWpZqjFpe6K+7e1x7Bfg49ms0MTFR77zzjiZPnqzmzZvr5MmT2rt3b45tXnrpJb366quKi4vTSy+9pJ49e2r//v3y8vLS1atX1aBBAw0fPlzBwcFaunSpevfurSpVqqhx48b2fcydO1dDhw7Vhg0btG7dOvXt21fNmjXTAw88YN9m7NixmjRpkl555RW9+eabeuyxx3TkyBGFhobq4sWLat26tZ566ilNnjxZV65c0fDhw9W9e3d9//33+XqfsrKyNG/ePElS/fr187UPR1FqAAAoQOnp6ZoyZYqmTZumhIQESVKVKlXUvHnzHNsNGzZMnTt3lnSjeNSsWVP79+9X9erVVb58eQ0bNsy+7XPPPafly5dr4cKFOUpNfHy8Ro8eLUmKi4vTtGnTtHLlyhylpm/fvurZs6ckKSkpSVOnTtXGjRvVoUMHTZs2TfXq1VNSUpJ9+9mzZysqKkr79u1T1apVHfqeU1NTFRgYKEm6cuWKvL29NXPmTFWpUsXh9y0/KDUAALfm7+2p3ePaG/K6jtizZ4+sVqvatGlz2+3i4+PtX9+8/lFKSoqqV6+urKwsJSUlaeHChTp+/LgyMzNltVpvObPyH/dxcz83r6mU2zYlSpRQcHCwfZtffvlFq1atsheSPzpw4IDDpSYoKEhbt26VJGVkZOi7775Tv379VLp0aXXp0sWhfeQHpQYA4NYsFovDh4GMcPO6Rnfi7e1t//rm2XWzs7MlSa+88oqmTJmiN954Q7Vr11aJEiU0ePBgZWZm5rmPm/u5uQ9Htrl06ZK6dOmiiRMn3pLPmQtNenh4KDY21n4/Pj5e3377rSZOnEipAQDAXcXFxcnf318rV67UU089la99rF27Vg899JB69eol6UbZ2bdvn2rUqOHKqKpfv74WLVqk6OhoeXm5tiJ4enrqypUrLt3n/+LTTwAAFCA/Pz8NHz5cL7zwgubNm6cDBw5o/fr1mjVrlsP7iIuL04oVK/Tzzz9rz549evbZZ3X69GmXZx0wYIDOnz+vnj17atOmTTpw4ICWL1+uxx9/XFlZWQ7vx2az6dSpUzp16pQOHTqkmTNnavny5XrooYdcnvmPmKkBAKCAvfzyy/Ly8tKoUaN04sQJRUREqF+/fg4/f+TIkTp48KDat2+vgIAAPfPMM+ratatSU1NdmjMyMlJr167V8OHD1a5dO1mtVlWqVEkdOnSQh4fj8yBpaWn2w1W+vr6qVKmSxo0bp+HDh7s07/+y2Jz5oL2bS0tLU0hIiFJTUxUcHGx0HACAk65evapDhw6pcuXK8vPzMzoOXOh2/7aO/v7m8BMAADAFtyk1M2bMUHx8vIKDgxUcHKwmTZrom2++MToWAAAoItym1FSoUEETJkzQli1btHnzZrVu3VoPPfSQfv31V6OjAQCAIsBtFgr/7+fax48frxkzZmj9+vWqWbOmQakAoIi7bpWuXJCCwo1OAhQ4tyk1f5SVlaVPPvlEly9fVpMmTfLczmq1ymq12u+npaUVRjwAMJbNJm3/UPpiwH8fazNaun+ocZmAQuBWpWbnzp1q0qSJrl69qsDAQC1evPi2Jx5KTk7W2LFjCzEhABjo2CZpwWPSpVzOX3Jia+HnAQqZ26ypkaRq1app+/bt2rBhg/r376+EhATt3r07z+0TExOVmppqvx07dqwQ0wJAIbiUIr3XWRoTIs1qm3uhAYoJt5qp8fHxsV9LokGDBtq0aZOmTJmit99+O9ftfX195evrW5gRAaDgXbdKK8dJ66blPh5RR+o2RwqNkTbNkpZy2AnFg1uVmv+VnZ2dY80MAJiWzSb98pH0ef/cx30Cpe7zpCqtpf+7GCLcw+HDh1W5cmVt27ZNdevWNTrOHbVs2VJ169bVG2+8YXSUW7hNqUlMTFTHjh1VsWJFpaena/78+frhhx+0fPlyo6MBQME5sk5a2Fu6fCb38fbJUuNnJE+3+XGOYuLKlSsqX768PDw8dPz48UI5cuI2/xekpKSoT58+OnnypEJCQhQfH6/ly5frgQceMDoaALjWxaPSFwOlQ6tzH6+fID0wTvIvWaixAGcsWrRINWvWlM1m0+eff64ePXoU+Gu6zULhWbNm6fDhw7JarUpJSdF3331HoQFgHpmXpa+G3ljw+0btWwtNZD3pua3SmFTpwakUGjeTnZ2tSZMmKTY2Vr6+vqpYsaLGjx+fY5uDBw+qVatWCggIUJ06dbRu3Tr72Llz59SzZ0+VL19eAQEBql27tj766KMcz2/ZsqUGDRqkF154QaGhoQoPD9eYMWNybGOxWPTuu+/q4YcfVkBAgOLi4rRkyZIc2+zatUsdO3ZUYGCgypUrp969e+vs2bNOf8+zZs1Sr1691KtXL6euSH433KbUAIDpZGdLG9+5UWSSIqXN//OD39NH6vPFjSLzzA9S6SqGxCzybLYbpbCwb05cDzoxMVETJkzQyy+/rN27d2v+/PkqV65cjm1eeuklDRs2TNu3b1fVqlXVs2dPXb9+XdKNiz02aNBAS5cu1a5du/TMM8+od+/e2rhxY459zJ07VyVKlNCGDRs0adIkjRs3TitWrMixzdixY9W9e3ft2LFDnTp10mOPPabz589Lki5evKjWrVurXr162rx5s5YtW6bTp0+re/fuTv2THDhwQOvWrVP37t3VvXt3/fTTTzpy5IhT+8gPtzn8BACmcXiN9NHfJGtq7uPtk6V7+0ke/N3pkGsZN0phYRtxQvIpccfN0tPTNWXKFE2bNk0JCQmSpCpVqqh58+Y5ths2bJg6d+4s6UbxqFmzpvbv36/q1aurfPnyGjZsmH3b5557TsuXL9fChQvVuHFj++Px8fEaPXq0JCkuLk7Tpk3TypUrcxzZ6Nu3r3r27ClJSkpK0tSpU7Vx40Z16NBB06ZNU7169ZSUlGTffvbs2YqKitK+fftUtWpVh96a2bNnq2PHjipVqpQkqX379nrvvfdumTlyNUoNABSGi8ekT/pKxzfnPl73ManTq5JPQKHGQsHbs2ePrFar2rRpc9vt4uPj7V9HRERIurGetHr16srKylJSUpIWLlyo48ePKzMzU1arVQEBAXnu4+Z+UlJS8tymRIkSCg4Otm/zyy+/aNWqVQoMDLwl34EDBxwqNVlZWZo7d66mTJlif6xXr14aNmyYRo0aJY8CLOuUGgAoKNZL0nejpU3v5j4edZ/0l5lSqUqFm8tsvANuzJoY8boO8Pf3d2x33t72ry3/97H87OxsSdIrr7yiKVOm6I033lDt2rVVokQJDR48WJmZmXnu4+Z+bu7DkW0uXbqkLl26aOLEibfku1m07mT58uU6fvz4LQuDs7Kybpk1cjVKDQC4Una2tHWO9NWQ3McDSks9PpAqNS3UWKZmsTh0GMgocXFx8vf318qVK/XUU0/lax9r167VQw89pF69ekm6UXb27dt320sF5Uf9+vW1aNEiRUdHy8srfxVh1qxZ+utf/6qXXnopx+Pjx4/XrFmzKDUAUOQd+unGdZeu5rFO5s9v3PgoNutkih0/Pz8NHz5cL7zwgnx8fNSsWTOdOXNGv/76q5588kmH9hEXF6dPP/1UP//8s0qVKqXXX39dp0+fdnmpGTBggN555x317NnT/imq/fv36+OPP9a7774rT0/P2z7/zJkz+vLLL7VkyRLVqlUrx1ifPn308MMP6/z58woNDXVp7psoNQCQX+cPSov7Scc25D5+bz+p9cuS763rE1C8vPzyy/Ly8tKoUaN04sQJRUREqF+/fg4/f+TIkTp48KDat2+vgIAAPfPMM+ratatSU/Mo0fkUGRmptWvXavjw4WrXrp2sVqsqVaqkDh06OLQWZt68eSpRokSu64fatGkjf39/ffDBBxo0aJBLc99ksdmc+Eyam0tLS1NISIhSU1MVHBxsdBwA7siaLn39gvTL/NzHKzaR/vKOVDKqcHPl5ea1n+7pcuOwl5u7evWqDh06pMqVK8vPz8/oOHCh2/3bOvr7m5kaALiT7Cxp3XRpxcu5j/uFSD0XSJWaFG4uADlQagAgL/tXSvN7SNnXch/v/LrU4HHWyQBFBKUGAP7o3AFpYYJ0emfu442eltr9S/J27GO6AAoPpQYArlyQlo+Utuex5iSmlfTQNCmkQuHmAuAUSg2A4inrurTxbWn5iNzHgyKl7nOlqMa5j8NQxegzLsWGK/5NKTUAig+bTTqwUvq4l3T9Su7bPPy2FN/jxgndUOTcPBtuRkaGw2fqhXvIyMiQdOsZj51BqQFgfmf3S58+Lp3akft4s8FSqxGSl2+hxoLzPD09VbJkSfu1igICAuyXFIB7stlsysjIUEpKikqWLHnHE/zdDqUGgDldTZWWDJJ2f577eOUWN84nE1SuUGPh7oWHh0vSLRdqhHsrWbKk/d82vyg1AMwj67q0drL0/b9zHw8Ml3rOl8o3KNxccCmLxaKIiAiVLVtW167l8XF7uBVvb++7mqG5iVIDwL3ZbNJv30gf98x7m64zpDo9WSdjMp6eni75RQjzoNQAcE+nd0ufJEhn9+U+3nSQ1OolyZtT6QPFBaUGgPu4fFZa9qK085Pcx6t1unGW3+CIws0FoEig1AAo2q5bpbVTpVV5rJMpFS09+p5Uvn6hxgJQ9FBqABQ9Npu0Z4n0SV/Jlp37Nt3mSDW6sk4GgB2lBkDRkbJHWtgn73UyLYZLf/qn5Jn/k3MBMC9KDQBjXU2VPntG2rcs9/G49tJf3pb8SxVuLgBuh1IDoPBlXZNWT5R+fCX38VLR0l/nS+VqFmosAO6NUgOgcNhs0q+Lb1yuIDcWD+mRWVLNh1knAyBfKDUACtaJbdLCBOnikdzHWwyX7n+e6y4BuGuUGgCul35KWvq8tPer3Mdr/kXqOEkKDCvcXABMjVIDwDWuXbmxTmbN5NzHw6pLj85mnQyAAkOpAZB/Npu0Y6G0+Jm8t/nrR1L1ToWXCUCxRakB4LwT26QFvaXUY7mPtx4pNRsiefIjBkDh4ScOAMdknL9xht9Dq3Mfv+fBG1fD9g0s1FgAcBOlBkDermdK3/9L+nlq7uNla0jd35fKxBZuLgDIBaUGQE53Wifj5X/juktV23M+GQBFCqUGwA3HNkkLe0vpJ3MfbztWajKA6y4BKLIoNUBxdvGY9OU/pAMrcx+v20tq9y8pILRwcwFAPlBqgOIm87K0cpy04a3cx8Pjb1yuIKxq4eYCgLtEqQGKg+xsadu8G7MyubF4SI99IsW2LdxcAOBClBrAzI6ulz7+m5RxLvfxB/51Y52Mh2fh5gKAAkCpAcwm7eSN88kcW5/7eHwP6c+TJZ8ShRoLAAqa25Sa5ORkffbZZ9q7d6/8/f3VtGlTTZw4UdWqVTM6GmC8a1ek78bkvU6mfIMb110qFV2YqQCgULlNqVm9erUGDBigRo0a6fr16xoxYoTatWun3bt3q0QJ/uJEMWSzSdvel5Y8l/u4X0mp+zwppkWhxgIAo7hNqVm2bFmO+3PmzFHZsmW1ZcsW/elPfzIoFWCAw2ulBb2kK+dzH+/0qtTwCdbJACh23KbU/K/U1FRJUmho3ufPsFqtslqt9vtpaWkFngsoMNlZUlJ56fqVW8caPSW1GSX5hRR+LgAoItyy1GRnZ2vw4MFq1qyZatWqled2ycnJGjt2bCEmAwpQ2vGchaZCI+kv70ihlY3LBABFiFuWmgEDBmjXrl1as2bNbbdLTEzU0KFD7ffT0tIUFRVV0PGAgjcm1egEAFDkuF2pGThwoL766iv9+OOPqlChwm239fX1la+vbyElAwqJl7/RCQCgSHKbUmOz2fTcc89p8eLF+uGHH1S5MlPuAADgv9ym1AwYMEDz58/XF198oaCgIJ06dUqSFBISIn9//nIFAKC48zA6gKNmzJih1NRUtWzZUhEREfbbggULjI4GAACKALeZqbHZbEZHAAAARZjbzNQAAADcDqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYAqUGAACYgluVmh9//FFdunRRZGSkLBaLPv/8c6MjAQCAIsKtSs3ly5dVp04dTZ8+3egoAACgiPEyOoAzOnbsqI4dOxodAwAAFEFuVWqcZbVaZbVa7ffT0tIMTAMAAAqSWx1+clZycrJCQkLst6ioKKMjAQCAAmLqUpOYmKjU1FT77dixY0ZHAgAABcTUh598fX3l6+trdAwAAFAITD1TAwAAig+3mqm5dOmS9u/fb79/6NAhbd++XaGhoapYsaKByQAAgNHcqtRs3rxZrVq1st8fOnSoJCkhIUFz5swxKBUAACgK3KrUtGzZUjabzegYAACgCHK61FitVm3YsEFHjhxRRkaGwsLCVK9ePVWuXLkg8gEAADjE4VKzdu1aTZkyRV9++aWuXbumkJAQ+fv76/z587JarYqJidEzzzyjfv36KSgoqCAzAwAA3MKhTz89+OCD6tGjh6Kjo/Xtt98qPT1d586d0++//66MjAz95z//0ciRI7Vy5UpVrVpVK1asKOjcAAAAOTg0U9O5c2ctWrRI3t7euY7HxMQoJiZGCQkJ2r17t06ePOnSkAAAAHfiUKl59tlnHd5hjRo1VKNGjXwHAgAAyA9OvgcAAEzBZaUmISFBrVu3dtXuAAAAnOKy89SUL19eHh5M/AAAAGO4rNQkJSW5alcAAABOY2oFAACYgtMzNU888cRtx2fPnp3vMAAAAPnldKm5cOFCjvvXrl3Trl27dPHiRRYKAwAAwzhdahYvXnzLY9nZ2erfv7+qVKniklAAAADOcsmaGg8PDw0dOlSTJ092xe4AAACc5rKFwgcOHND169ddtTsAAACnOH34aejQoTnu22w2nTx5UkuXLlVCQoLLggEAADjD6VKzbdu2HPc9PDwUFham11577Y6fjAIAACgoTpeaVatWFUQOAACAu8LJ9wAAgCm4rNSMGDGCw08AAMAwLrv20/Hjx3Xs2DFX7Q4AAMApLis1c+fOddWuAAAAnMaaGgAAYAr5mqm5fPmyVq9eraNHjyozMzPH2KBBg1wSDAAAwBn5Ok9Np06dlJGRocuXLys0NFRnz55VQECAypYtS6kBAACGcPrw05AhQ9SlSxdduHBB/v7+Wr9+vY4cOaIGDRro1VdfLYiMAAAAd+R0qdm+fbuef/55eXh4yNPTU1arVVFRUZo0aZJGjBhREBkBAADuyOlS4+3tLQ+PG08rW7asjh49KkkKCQnhI90AAMAwTq+pqVevnjZt2qS4uDi1aNFCo0aN0tmzZ/X++++rVq1aBZERAADgjpyeqUlKSlJERIQkafz48SpVqpT69++vM2fOaObMmS4PCAAA4AinZ2oaNmxo/7ps2bJatmyZSwMBAADkByffAwAApuBQqenQoYPWr19/x+3S09M1ceJETZ8+/a6DAQAAOMOhw0/dunXTI488opCQEHXp0kUNGzZUZGSk/Pz8dOHCBe3evVtr1qzR119/rc6dO+uVV14p6NwAAAA5OFRqnnzySfXq1UuffPKJFixYoJkzZyo1NVWSZLFYVKNGDbVv316bNm3SPffcU6CBAQAAcuPwQmFfX1/16tVLvXr1kiSlpqbqypUrKl26tLy9vQssIAAAgCPydUFL6cbJ9kJCQlyZBQAAIN/49BMAADAFSg0AADAFSg0AADAFtys106dPV3R0tPz8/HTvvfdq48aNRkcCAABFQL5KzcWLF/Xuu+8qMTFR58+flyRt3bpVx48fd2m4/7VgwQINHTpUo0eP1tatW1WnTh21b99eKSkpBfq6AACg6HP60087duxQ27ZtFRISosOHD+vpp59WaGioPvvsMx09elTz5s0riJySpNdff11PP/20Hn/8cUnSW2+9paVLl2r27Nl68cUXC+x1C8KZE4d1/ZrV6BhwI56XTqqsJJski9Fh4HauZqTrwpHfjI4BNxNUqqwCg0sZHcNhTpeaoUOHqm/fvpo0aZKCgoLsj3fq1El/+9vfXBrujzIzM7VlyxYlJibaH/Pw8FDbtm21bt26XJ9jtVpltf63OKSlpRVYPmdlvPtnVco+ZnQMuKHM69nyNToE3MaR8xmqJMnvyA+KeK+x0XHgZjbWHqPGjwwxOobDnC41mzZt0ttvv33L4+XLl9epU6dcEio3Z8+eVVZWlsqVK5fj8XLlymnv3r25Pic5OVljx44tsEx347rFW1dtnLQQzvvK1kSPGh0CbuMXz1ryspVWaRWdP+rgRjw8jU7gFKdLja+vb64zHvv27VNYWJhLQrlKYmKihg4dar+flpamqKgoAxP9V5WXtxkdAW7m9wsZaj5xlfy8PSg1cFhaYIyaWd9Uh5rheqt3A6PjwM2429ye0wuFH3zwQY0bN07Xrl2TdOPaT0ePHtXw4cP1yCOPuDzgTWXKlJGnp6dOnz6d4/HTp08rPDw81+f4+voqODg4xw0AAJiT06Xmtdde06VLl1S2bFlduXJFLVq0UGxsrIKCgjR+/PiCyChJ8vHxUYMGDbRy5Ur7Y9nZ2Vq5cqWaNGlSYK8LAADcg9OHn0JCQrRixQqtWbNGO3bs0KVLl1S/fn21bdu2IPLlMHToUCUkJKhhw4Zq3Lix3njjDV2+fNn+aSgAAFB85fuCls2bN1fz5s1dmeWOevTooTNnzmjUqFE6deqU6tatq2XLlt2yeBgAABQ/TpeaqVOn5vq4xWKRn5+fYmNj9ac//UmengWzYnrgwIEaOHBggewbAAC4L6dLzeTJk3XmzBllZGSoVKkbJ+S5cOGCAgICFBgYqJSUFMXExGjVqlVF5pNGAADA/JxeKJyUlKRGjRrpP//5j86dO6dz585p3759uvfeezVlyhQdPXpU4eHhGjLEfU7WAwAA3J/TMzUjR47UokWLVKVKFftjsbGxevXVV/XII4/o4MGDmjRpUoF+vBsAAOB/OT1Tc/LkSV2/fv2Wx69fv24/o3BkZKTS09PvPh0AAICDnC41rVq10rPPPqtt2/57Rtxt27apf//+at26tSRp586dqly5sutSAgAA3IHTpWbWrFkKDQ1VgwYN5OvrK19fXzVs2FChoaGaNWuWJCkwMFCvvfaay8MCAADkxek1NeHh4VqxYoX27t2rffv2SZKqVaumatWq2bdp1aqV6xICAAA4IN8n36tevbqqV6/uyiwAAAD5lq9S8/vvv2vJkiU6evSoMjMzc4y9/vrrLgkGAADgDKdLzcqVK/Xggw8qJiZGe/fuVa1atXT48GHZbDbVr1+/IDICAADckdMLhRMTEzVs2DDt3LlTfn5+WrRokY4dO6YWLVqoW7duBZERAADgjpwuNXv27FGfPn0kSV5eXrpy5YoCAwM1btw4TZw40eUBAQAAHOF0qSlRooR9HU1ERIQOHDhgHzt79qzrkgEAADjB6TU19913n9asWaN77rlHnTp10vPPP6+dO3fqs88+03333VcQGQEAAO7I6VLz+uuv69KlS5KksWPH6tKlS1qwYIHi4uL45BMAADCM06UmJibG/nWJEiX01ltvuTQQAABAfji9piYmJkbnzp275fGLFy/mKDwAAACFyelSc/jwYWVlZd3yuNVq1fHjx10SCgAAwFkOH35asmSJ/evly5crJCTEfj8rK0srV65UdHS0S8MBAAA4yuFS07VrV0mSxWJRQkJCjjFvb29FR0dzZW4AAGAYh0tNdna2JKly5cratGmTypQpU2ChAAAAnOX0p58OHTpUEDkAAADuikOlZurUqQ7vcNCgQfkOAwAAkF8OlZrJkyc7tDOLxUKpAQAAhnCo1HDICQAAFHVOn6fmj2w2m2w2m6uyAAAA5Fu+Ss28efNUu3Zt+fv7y9/fX/Hx8Xr//fddnQ0AAMBh+bqg5csvv6yBAweqWbNmkqQ1a9aoX79+Onv2rIYMGeLykAAAAHfidKl58803NWPGDPXp08f+2IMPPqiaNWtqzJgxlBoAAGAIpw8/nTx5Uk2bNr3l8aZNm+rkyZMuCQUAAOAsp0tNbGysFi5ceMvjCxYsUFxcnEtCAQAAOMvpw09jx45Vjx499OOPP9rX1Kxdu1YrV67MtewAAAAUBodnanbt2iVJeuSRR7RhwwaVKVNGn3/+uT7//HOVKVNGGzdu1MMPP1xgQQEAAG7H4Zma+Ph4NWrUSE899ZT++te/6oMPPijIXAAAAE5xeKZm9erVqlmzpp5//nlFRESob9+++umnnwoyGwAAgMMcLjX333+/Zs+erZMnT+rNN9/UoUOH1KJFC1WtWlUTJ07UqVOnCjInAADAbTn96acSJUro8ccf1+rVq7Vv3z5169ZN06dPV8WKFfXggw8WREYAAIA7uqtrP8XGxmrEiBEaOXKkgoKCtHTpUlflAgAAcIrTH+m+6ccff9Ts2bO1aNEieXh4qHv37nryySddmQ0AAMBhTpWaEydOaM6cOZozZ47279+vpk2baurUqerevbtKlChRUBkBAADuyOFS07FjR3333XcqU6aM+vTpoyeeeELVqlUryGwAAAAOc7jUeHt769NPP9Wf//xneXp6FmSmXI0fP15Lly7V9u3b5ePjo4sXLxZ6BgAAUHQ5XGqWLFlSkDnuKDMzU926dVOTJk00a9YsQ7MAAICiJ98LhQvb2LFjJUlz5sxx+DlWq1VWq9V+Py0tzdWxAABAEXFXH+ku6pKTkxUSEmK/RUVFGR0JAAAUEFOXmsTERKWmptpvx44dMzoSAAAoIIaWmhdffFEWi+W2t7179+Z7/76+vgoODs5xAwAA5mTomprnn39effv2ve02MTExhRMGAAC4NUNLTVhYmMLCwoyMAAAATMJtPv109OhRnT9/XkePHlVWVpa2b98u6cb1pwIDA40NBwAADOc2pWbUqFGaO3eu/X69evUkSatWrVLLli0NSgUAAIoKt/n005w5c2Sz2W65UWgAAIDkRqUGAADgdig1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFCg1AADAFNyi1Bw+fFhPPvmkKleuLH9/f1WpUkWjR49WZmam0dEAAEAR4WV0AEfs3btX2dnZevvttxUbG6tdu3bp6aef1uXLl/Xqq68aHQ8AABQBblFqOnTooA4dOtjvx8TE6LffftOMGTMoNSh2rl7L1tVrWfLz9jQ6CgAUKW5RanKTmpqq0NDQ225jtVpltVrt99PS0go6FlBgPCwW+9fVX14mSXqjR109VDdSlj+MAUBx5RZrav7X/v379eabb+rZZ5+97XbJyckKCQmx36KiogopIeB6ESF+qlexZI7HBi/YrsqJX6tp8kptO3rBmGAAUEQYWmpefPFFWSyW29727t2b4znHjx9Xhw4d1K1bNz399NO33X9iYqJSU1Ptt2PHjhXktwMUKIvFosV/b6ZfRrXTI/Ur5Bg7kXpVD/+/nxX94lL1nrVBp1KvGpQSAIxjsdlsNqNe/MyZMzp37txtt4mJiZGPj48k6cSJE2rZsqXuu+8+zZkzRx4eznWytLQ0hYSEKDU1VcHBwfnODRQVh85e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FBYWlq/nZmdnS1KOw0sAAKD4coszCm/YsEGbNm1S8+bNVapUKR04cEAvv/yyqlSp4vAsDQAAMDe3+PRTQECAPvvsM7Vp00bVqlXTk08+qfj4eK1evVq+vr5GxwMAAEWAW8zU1K5dW99//73RMQAAQBHmFjM1AAAAd0KpAQAApkCpAQAApkCpAQAApkCpAQAApkCpAQAApkCpAQAApkCpAQAApkCpAQAApkCpAQAApkCpAQAApkCpAQAApkCpAQAApkCpAQAApkCpAQAApkCpAQAApkCpAQAApkCpAQAApkCpAQAApkCpAQAApkCpAQAApkCpAQAApuBldIDCZLPZJElpaWkGJwEAAI66+Xv75u/xvBSrUpOeni5JioqKMjgJAABwVnp6ukJCQvIct9juVHtMJDs7WydOnFBQUJAsFouhWdLS0hQVFaVjx44pODjY0CxFDe9N3nhv8sZ7kzfem9zxvuStqL03NptN6enpioyMlIdH3itnitVMjYeHhypUqGB0jByCg4OLxH8wRRHvTd54b/LGe5M33pvc8b7krSi9N7ebobmJhcIAAMAUKDUAAMAUKDUG8fX11ejRo+Xr62t0lCKH9yZvvDd5473JG+9N7nhf8uau702xWigMAADMi5kaAABgCpQaAABgCpQaAABgCpQaAABgCpQag0yfPl3R0dHy8/PTvffeq40bNxodyXA//vijunTposjISFksFn3++edGRyoykpOT1ahRIwUFBals2bLq2rWrfvvtN6NjGW7GjBmKj4+3nyCsSZMm+uabb4yOVSRNmDBBFotFgwcPNjqK4caMGSOLxZLjVr16daNjFRnHjx9Xr169VLp0afn7+6t27dravHmz0bEcQqkxwIIFCzR06FCNHj1aW7duVZ06ddS+fXulpKQYHc1Qly9fVp06dTR9+nSjoxQ5q1ev1oABA7R+/XqtWLFC165dU7t27XT58mWjoxmqQoUKmjBhgrZs2aLNmzerdevWeuihh/Trr78aHa1I2bRpk95++23Fx8cbHaXIqFmzpk6ePGm/rVmzxuhIRcKFCxfUrFkzeXt765tvvtHu3bv12muvqVSpUkZHc4wNha5x48a2AQMG2O9nZWXZIiMjbcnJyQamKlok2RYvXmx0jCIrJSXFJsm2evVqo6MUOaVKlbK9++67RscoMtLT021xcXG2FStW2Fq0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AKB48/b2znHfYrHkeOxmEcnOzpYkXbp0SV26dNHEiRNv2VdERESur1GmTBllZGQoMzNTPj4+d3z9m69Vv359HTp0SN98842+++47de/eXW3bttWnn35q3/78+fMKCwtz9NsFUIAoNQDcSv369bVo0SJFR0fLy8uxH2F169aVJO3evdv+taOCg4PVo0cP9ejRQ48++qg6dOig8+fPKzQ0VNKNRcv16tVzap8ACgaHnwC4lQEDBuj8+fPq2bOnNm3apAMHDmj58uV6/PHHlZWVletzwsLCVL9+fa1Zs8ap13r99df10Ucfae/evdq3b58++eQThYeH5zjPzk8//aR27drdzbcEwEUoNQDcSmRkpNauXausrCy1a9dOtWvX1uDBg1WyZEl5eOT9I+2pp57Shx9+6NRrBQUFadKkSWrYsKEaNWqkw4cP6+uvv7a/zrp165SamqpHH330rr4nAK7ByfcAFAtXrlxRtWrVtGDBAjVp0sQl++zRo4fq1KmjESNGuGR/AO4OMzUAigV/f3/Nmzfvtifpc0ZmZqZq166tIUOGuGR/AO4eMzUAAMAUmKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACm8P8BVNLy6vA1a2EAAAAASUVORK5CYII=",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from qupulse.pulses.plotting import plot\n",
+ "from qupulse.pulses import TablePT\n",
+ "\n",
+ "param_entries = {'A': [(0, 0),\n",
+ " ('ta', 'va', 'hold'),\n",
+ " ('tb', 'vb', 'linear'),\n",
+ " ('tend', 0, 'jump')],\n",
+ " 'B': [(0, 0),\n",
+ " ('ta', '-va', 'hold'),\n",
+ " ('tb', '-vb', 'linear'),\n",
+ " ('tend', 0, 'jump')]}\n",
+ "mirror_pulse = TablePT(param_entries)\n",
+ "\n",
+ "parameters = {'ta': 2,\n",
+ " 'va': 2,\n",
+ " 'tb': 4,\n",
+ " 'vb': 3,\n",
+ " 'tend': 6}\n",
+ "\n",
+ "_ = plot(mirror_pulse, parameters, sample_rate=100)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "You may have noticed that we already used an expression in the entry list: `'-va'` and `'-vb'`. Of course we can also do a bit more complex things with these than a simple negation. Let's have a look at the next example where we use some simple mathematical oeprators and built-in functions:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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0KBBqlfP+XpFfn6+fagoNDRULVq00FNPPaUpU6a4Nd4L2QxXJo1bTH5+vmJjY5WXl6eYmBizwwEAuOinn37SgQMH1KpVK4WFhZkdDtzoYv+2rnx+M/QDAAAsi0QFAABYFokKAACwLJppAX9lGFJpYcXz4AjplzUcAMCXkKgA/sgwpLcGSkc2V2wnXyndvZJkBYDPYegH8EelheeSFEk6skkqOFGRwACADyFRAQLF/BTprUEkKwB8CokK4O8S0s89p7ICwMeQqAD+LmOlNHn/uW0qK4DHHTx4UDabTdu3bzc7FKf06dNHEyZMMDsMh0hUAH9ns0mR8RUNtZWorACohaKiIsXFxSk+Pl7FxcVeeU8SFSAQ2GwVs36orACog+XLl6tDhw5KS0vTBx984JX3JFEBAgWVFcCtysvLNW/ePKWkpCg0NFTNmzfXrFmzqhzz7bff6vrrr1dERIQ6d+6sjRs32l87efKkRowYoWbNmikiIkLp6elaunRpla/v06ePxo8fr0cffVRxcXFKSEjQE088UeUYm82mN954Q7feeqsiIiKUmpqqDz/8sMoxu3bt0uDBgxUVFaWmTZtq1KhROnHihMvf85tvvqmRI0dq5MiRLt0pui5IVIBAQmUFvsAwpJICcx4u/B5MnTpVf/zjHzV9+nTt3r1b7777rpo2bVrlmMcff1yTJ0/W9u3b1bZtW40YMUI///yzpIqb9nXr1k0fffSRdu3apfvvv1+jRo3SF198UeUcixcvVmRkpDZv3qx58+bpqaee0urVq6sc8+STT2rYsGHasWOHbrjhBt155506deqUJOn06dPq27evunTpoq1bt2rlypU6duyYhg0b5tI/S3Z2tjZu3Khhw4Zp2LBhWrdunQ4dOuTSOWqDuycD/qikQJqdVPH8saNSSGTV1w2jIjk5suncvsn7KyouLAoHL3J4h93zf369zdHviwNnzpxR48aNtXDhQt17773VXj948KBatWqlN954Q/fcc48kaffu3erQoYP27NmjtLQ0h+e96aablJaWpvnz50uqqKiUlZVp3bp19mN69uypvn376o9//KOkiorKtGnT9PTTT0uSCgoKFBUVpY8//liDBg3SM888o3Xr1mnVqlX2c3z33XdKTk7W3r171bZtW/Xp00dXXHGFFixYUOP3/Pjjj2v37t1asWKFJGno0KG64oor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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "expr_pulse = TablePT({'A': [(0, 'a_0'),\n",
+ " ('t_1', 'a_0 + exp(theta)', 'hold'),\n",
+ " ('t_2', 'Abs(x_0 - y_0)', 'linear')],\n",
+ " 'B': [(0, 'b_0'),\n",
+ " ('t_1*(b_0/a_0)', 'b_1', 'linear'),\n",
+ " ('t_2', 'b_2')]})\n",
+ "_ = plot(expr_pulse, dict(a_0=1.1, theta=2, x_0=0.5, y_0=1, t_1=10, t_2=25, b_0=0.6, b_1=6, b_2=0.4))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ " __Is there a requirement that all channels have the same duration?__\n",
+ " \n",
+ " No. The shorter channels stay on their last value until the last channel is finished. The duration of the complete pulse template is given as the corresponding expression:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Max(t_A, t_B)\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjcAAAGwCAYAAABVdURTAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAA5gUlEQVR4nO3de3RTZdr+8SstpecWSukBLCepMGA5FrDAyEGGigyKo8CwlJOi4qAMdljyVhHEURA8geIrMsiLzIwjKsIwoiBW5GQVEDqKKAgCRWg509IWW2zy+4NfI6EpJG3KTna+n7WyVrOzs3MnQHOxn2c/t8Vms9kEAABgEgFGFwAAAOBJhBsAAGAqhBsAAGAqhBsAAGAqhBsAAGAqhBsAAGAqhBsAAGAqdYwu4GqzWq06cuSIIiMjZbFYjC4HAAC4wGaz6ezZs2rUqJECAi5/bsbvws2RI0eUlJRkdBkAAKAaDh06pGuuueay+/hduImMjJR04cOJiooyuBoAAOCKwsJCJSUl2b/HL8fvwk3FUFRUVBThBgAAH+PKlBImFAMAAFMh3AAAAFMh3AAAAFPxuzk3riovL9f58+eNLgMeEBQUpMDAQKPLAABcJYSbS9hsNuXn5+vMmTNGlwIPqlevnhISEljbCAD8AOHmEhXBJi4uTmFhYXwZ+jibzaaSkhIdO3ZMkpSYmGhwRQCA2ka4uUh5ebk92DRo0MDocuAhoaGhkqRjx44pLi6OISoAMDkmFF+kYo5NWFiYwZXA0yr+TJlHBQDmR7hxgqEo8+HPFAD8B+EGAACYCuEGAACYCuHGDxw4cEAWi0U5OTlGl+KS3r17a+LEiUaXAQDwUYQb+JzFixfLYrHYbxEREercubPef/99o0sDAHgBwg18UlRUlPLy8pSXl6cdO3YoPT1dQ4cO1e7du40uDQBgMMLNFdhsNpWU/WLIzWazuVyn1WrV7Nmz1bJlSwUHB6tJkyZ65plnHPb58ccf1adPH4WFhal9+/bKzs62P3by5EkNHz5cjRs3VlhYmFJSUvSvf/3L4fm9e/fWhAkT9OijjyomJkYJCQl68sknHfaxWCxauHChbr/9doWFhSk5OVkrV6502Gfnzp0aMGCAIiIiFB8frxEjRujEiRMuv9eK10lISFBCQoKSk5P19NNPKyAgQF9//bVbxwEAmA+L+F3BufPlajN1jSGvveupdIXVde2PKDMzU3/729/00ksvqWfPnsrLy9P333/vsM/jjz+u559/XsnJyXr88cc1fPhw7d27V3Xq1NHPP/+szp07a/LkyYqKitKqVas0YsQIXXvtteratav9GG+++aYyMjL05ZdfKjs7W6NHj1aPHj30u9/9zr7P9OnTNXv2bD333HN65ZVXdNddd+ngwYOKiYnRmTNn1LdvX40dO1YvvfSSzp07p8mTJ2vo0KH69NNPq/U5lZeXa8mSJZKkTp06VesYAADzINyYwNmzZzV37lzNmzdPo0aNkiRde+216tmzp8N+kyZN0sCBAyVdCCBt27bV3r171bp1azVu3FiTJk2y7/vwww9rzZo1eueddxzCTbt27TRt2jRJUnJysubNm6esrCyHcDN69GgNHz5ckjRjxgy9/PLL2rJli26++WbNmzdPHTt21IwZM+z7L1q0SElJSdqzZ4+uu+46l95zQUGBIiIiJEnnzp1TUFCQFixYoGuvvdblzw0AYE6EmysIDQrUrqfSDXttV3z33XcqLS3VTTfddNn92rVrZ/+5osfSsWPH1Lp1a5WXl2vGjBl65513dPjwYZWVlam0tLTSas0XH6PiOBV9m5ztEx4erqioKPs+//3vf7Vu3Tp7MLnYvn37XA43kZGR2r59uySppKREn3zyicaNG6cGDRpo0KBBLh0DAGBOhJsrsFgsLg8NGaWid9KVBAUF2X+uWLHXarVKkp577jnNnTtXc+bMUUpKisLDwzVx4kSVlZVVeYyK41Qcw5V9ioqKNGjQIM2aNatSfe40tQwICFDLli3t99u1a6ePP/5Ys2bNItwAgJ/z7m9tuCQ5OVmhoaHKysrS2LFjq3WMzZs367bbbtPdd98t6ULo2bNnj9q0aePJUtWpUyctW7ZMzZo1U506nv3rFxgYqHPnznn0mAAA38PVUiYQEhKiyZMn69FHH9WSJUu0b98+ffHFF3rjjTdcPkZycrLWrl2rzz//XN99950eeOABHT161OO1jh8/XqdOndLw4cO1detW7du3T2vWrNGYMWNUXl7u8nFsNpvy8/OVn5+v/fv3a8GCBVqzZo1uu+02j9cMAPAtnLkxiSeeeEJ16tTR1KlTdeTIESUmJmrcuHEuP3/KlCn68ccflZ6errCwMN1///0aPHiwCgoKPFpno0aNtHnzZk2ePFn9+/dXaWmpmjZtqptvvlkBAa5n7cLCQvswVnBwsJo2baqnnnpKkydP9mi9AADfY7G5s5iKCRQWFio6OloFBQWKiopyeOznn3/W/v371bx5c4WEhBhUIWoDf7YA4Nsu9/19KYalAACAqRgabmbOnKkuXbooMjJScXFxGjx4sEvL57/77rtq3bq1QkJClJKSog8//PAqVAsAAHyBoeFm/fr1Gj9+vL744gutXbtW58+fV//+/VVcXFzlcz7//HMNHz5c9957r3bs2KHBgwdr8ODB2rlz51WsHAAAeCuvmnNz/PhxxcXFaf369brxxhud7jNs2DAVFxfrgw8+sG+74YYb1KFDB82fP/+Kr8GcG//Eny1gQjabdL7E6Cp8ns1m07nzF65WDQ2LlMWNizuuJnfm3HjV1VIVV+bExMRUuU92drYyMjIctqWnp2vFihVO9y8tLVVpaan9fmFhYc0LBQAYy2qVFtwo5X9jdCU+zyKpYi36kkm5CouINrIcj/CaeGa1WjVx4kT16NFD119/fZX75efnKz4+3mFbfHy88vPzne4/c+ZMRUdH229JSUkerRsAcJVZrdK8VIINquQ1Z27Gjx+vnTt3atOmTR49bmZmpsOZnsLCQgIOAPiqimBzat+F+zHXSg9skP5/Sxm4xmq1aeArm3Tg5K9zXDs1qa9/hEUaWJXneEW4eeihh/TBBx9ow4YNuuaaay67b0JCQqWVc48ePaqEhASn+wcHBys4ONhjtQIADOIs2Dy0TfLSOSLeyGazqaSsXL+ft0n7T5ZLClHz2HB98HBPhdUNtPcd9HWG/o2w2Wx66KGHtHz5cn366adq3rz5FZ+TlpamrKwsh21r165VWlpabZXp8w4cOCCLxaKcnByjS3FJ7969NXHiRKPLAOAtbDaptIhgU0NWq00DX96kttPWaP+JC2dsmseGKyujl8KD65gm2EgGn7kZP3683nrrLf373/9WZGSkfd5MdHS0vdP1yJEj1bhxY82cOVOS9Oc//1m9evXSCy+8oIEDB+rtt9/Wtm3btGDBAsPeB4xx7tw5NW7cWAEBATp8+DBn6AAzstmkRenSoS9/3UawcZvVatNNL663hxpJapMYpQ8e7qmAAPOEmgqG/s147bXXVFBQoN69eysxMdF+W7p0qX2f3Nxc5eXl2e93795db731lhYsWKD27dvrvffe04oVKy47CRnmtGzZMrVt21atW7eu8mo5AD6urNgx2CSkEGzcYLPZVFz6i0OwaR4brm+np2vVBHMGG8kLhqWc3UaPHm3f57PPPtPixYsdnjdkyBDt3r1bpaWl2rlzp2655ZarW7gXslqtmj17tlq2bKng4GA1adJEzzzzjMM+P/74o/r06aOwsDC1b99e2dnZ9sdOnjyp4cOHq3HjxgoLC1NKSor+9a9/OTy/d+/emjBhgh599FHFxMQoISFBTz75pMM+FotFCxcu1O23366wsDAlJydr5cqVDvvs3LlTAwYMUEREhOLj4zVixAidOHHC7ff8xhtv6O6779bdd9/tVgd0AD6gYijq9YvWPJu0V3pgI8HGRf40DHUp/oZcic124X8ORtzcWF8xMzNTzz77rJ544gnt2rVLb731VqVL5h9//HFNmjRJOTk5uu666zR8+HD98ssvki4scte5c2etWrVKO3fu1P33368RI0Zoy5YtDsd48803FR4eri+//FKzZ8/WU089pbVr1zrsM336dA0dOlRff/21brnlFt111106deqUJOnMmTPq27evOnbsqG3btmn16tU6evSohg4d6tYfy759+5Sdna2hQ4dq6NCh2rhxow4ePOjWMQB4KatVev230szGv86xSUiRwmO5KspFFcNQu/J+XdutTWKUsjJ6mfZszcW8aoXiq8HtFYrLiqUZjQyoVNJjR6S64Vfc7ezZs2rYsKHmzZunsWPHVnr8wIEDat68uRYuXKh7771XkrRr1y61bdtW3333nVq3bu30uL///e/VunVrPf/885IunLkpLy/Xxo0b7ft07dpVffv21bPPPivpwpmbKVOm6K9//askqbi4WBEREfroo49088036+mnn9bGjRu1Zs0a+zF++uknJSUlaffu3bruuuvUu3dvdejQQXPmzKnyPT/++OPatWuXli9fLkkaPHiwOnToUOlMUgVWKAZ8hM12IdhcvIZNQop0/wbO2LjAfjXUK5scztaY4WoouoL7me+++06lpaW66aabLrtfu3bt7D8nJiZKko4dOyZJKi8v11//+lelpKQoJiZGERERWrNmjXJzc6s8RsVxKo7hbJ/w8HBFRUXZ9/nvf/+rdevWKSIiwn6rCFf79u1z6f2Wl5frzTff1N13323fdvfdd2vx4sWyWq0uHQOAF7LZpOITvwabmGulzMMMRbnIn4ehLuUV69x4taCwC2dQjHptF1RcWXbFwwUF2X+u+EteEQaee+45zZ07V3PmzFFKSorCw8M1ceJElZWVVXmMiuNcGigut09RUZEGDRqkWbNmVaqvInBdyZo1a3T48GENGzbMYXt5ebmysrL0u9/9zqXjAPAiztopPLBBCo4wriYf4exsjWTuq6GuhHBzJRaLS0NDRkpOTlZoaKiysrKcDku5YvPmzbrtttvsZ0OsVqv27NmjNm3aeLJUderUScuWLVOzZs1Up071/vq98cYb+uMf/6jHH3/cYfszzzyjN954g3AD+JpLF+eTpKQbvP53rzewWm36/SubHObWmGUYqiYINyYQEhKiyZMn69FHH1XdunXVo0cPHT9+XN9++619js2VJCcn67333tPnn3+u+vXr68UXX9TRo0c9Hm7Gjx+vv/3tbxo+fLj9qqu9e/fq7bff1sKFCxUYGHjZ5x8/flz/+c9/tHLlykqX/48cOVK33367Tp06ddnmqwC8SFXtFOqGM3n4Cvxt7Rp3EG5M4oknnlCdOnU0depUHTlyRImJiRo3bpzLz58yZYp+/PFHpaenKywsTPfff78GDx5s79TuKY0aNdLmzZs1efJk9e/fX6WlpWratKluvvlmBbgwpr5kyRKFh4c7nV900003KTQ0VP/4xz80YcIEj9YNoBbQTqFazDxp2FO4WuoiXFFjXvzZAl6GYFMtVQ1D+cMl3u5cLcWZGwDA1UWwqRaGoVxHuAEAXB0Vi6K+fiPBxg0MQ7mPcAMAqH00wKwWfx6GqgnCDQCg9jlrgMmqw5fFMFT1EW6c8LM51n6BP1PAIBcPRVWYtJc+UZfBMFTNEW4uUrGybklJicur/sI3lJSUSKq8ejKAWuRs1WEaYF4Ww1CeQbi5SGBgoOrVq2fvgxQWFkZC9nE2m00lJSU6duyY6tWrd8VFAgF4iLNVhyuGovi96hTDUJ5DuLlEQkKCJFVqBgnfVq9ePfufLYBaZrNdOGPDqsMuuzTYMAxVM4SbS1gsFiUmJiouLk7nz583uhx4QFBQEGdsgKvFWWdvroiqUlXzaxiGqhnCTRUCAwP5QgQAd1TV2ZtgU0lFqBkyP5v5NbWAcAMAqDk6e7vMZrPpzvnZ+urgaYftzK/xHMINAKBm6OztMpvNppPFZQ7Bpk1ilN4dl8b8Gg8i3AAAqo8+US6pahhq25R+ahBel1DjYYQbAED1EGxc4mztGklKbVqfYFNLCDcAAPfQANNlVa1dwzBU7SLcAABcRwNMl7F2jXEINwAA19EA84pYu8Z4hBsAgGusVhpgXgG9obwD4QYAcHnO5tjQALMSekN5D8INAKBqzlYdjrmWBpgXqWoYivk1xiHcAACcq2iAeXGwYY6NA4ahvBPhBgBQmbMGmKw67IBhKO9FuAEAOKqqAWZwhHE1eRGGobwf4QYA8CsaYF4Ww1C+gXADALiABpiXxTCU7yDcAADoE3UZDEP5HsINAPg7gk2VGIbyTYQbAPBnBBunnJ2tkRiG8hWEGwDwVwQbp2w2m+6cn62vDp62b2MYyrcQbgDAHxFsnLLZbDpZXOYQbDhb43sINwDgbwg2lVQMQw2Zn+0wv2bblH5qEF6XszU+hnADAP7CWQNMgo3TScOSlNq0PsHGRxFuAMAfVNUAk2DjdO2ad8elMb/GhxFuAMDsaIBZCWvXmBvhBgDMrqyYBpgXYe0a8yPcAIBZXTzHpoKfN8CkhYJ/INwAgBk5m2OTkOLXDTAvDTYMQ5kX4QYAzMZZZ++KOTZ++CVe1fwahqHMi3ADAGZCZ28HzK/xT4QbADALFudzwPwa/0W4AQAzINjYcZk3CDcA4MtYddiuqhYKDEP5H8INAPgqm01alC4d+vLXbX4cbC7t5C0xDOWvCDcA4KvKih2DjZ+uOlxVJ29aKPgvwg0A+CKr1XFxvkl7pfBYv7siytnVUHTyBuEGAHyJszk2CSl+F2ycTRqW6OSNCwg3AOArqurs7WeL81W1dg1XQ6EC4QYAfAGdvSWxdg1cQ7gBAG9ns0nFJ/y6szdr18AdhBsA8GbOhqL8rLM3LRTgLsINAHgrZw0wk27wq87eDEOhOgg3AOCN/LwBJsNQqAnCDQB4Gz/vE8UwFGqKcAMA3oRgwzAUaoxwAwDewo+DDcNQ8CTCDQB4Az8ONgxDwdMINwBgND8PNgxDwdMINwBgJD8NNgxDoTYZ+q9nw4YNGjRokBo1aiSLxaIVK1Zcdv/PPvtMFoul0i0/P//qFAwAnmKzSaVFfhlsrFabBr68SW2nrXEINlkZvRQeXIdggxoz9MxNcXGx2rdvr3vuuUd/+MMfXH7e7t27FRUVZb8fFxdXG+UBQO2oqgGmyYNNVZ28GYaCpxkabgYMGKABAwa4/by4uDjVq1fP8wUBQG3z0waYNptNd87P1lcHT9u3MQyF2uKTc246dOig0tJSXX/99XryySfVo0ePKvctLS1VaWmp/X5hYWGV+wJArfLTBpg2m00ni8scgg1na1CbfCrcJCYmav78+UpNTVVpaakWLlyo3r1768svv1SnTp2cPmfmzJmaPn36Va4UAC7hhw0wK4ahhszPdrjMe9uUfmoQXpezNag1FpvNZjO6CEmyWCxavny5Bg8e7NbzevXqpSZNmujvf/+708ednblJSkpSQUGBw7wdAKg1VTXAvGe1ac/YOFu7RpJSm9bXu+PSCDZwW2FhoaKjo136/vapMzfOdO3aVZs2bary8eDgYAUHB1/FigDgIn7YALOqtWveHZfG/BpcFT4fbnJycpSYmGh0GQBQmZ+tYcPaNfAWhoaboqIi7d27135///79ysnJUUxMjJo0aaLMzEwdPnxYS5YskSTNmTNHzZs3V9u2bfXzzz9r4cKF+vTTT/Xxxx8b9RYAwDk/Cza0UIA3MTTcbNu2TX369LHfz8jIkCSNGjVKixcvVl5ennJzc+2Pl5WV6S9/+YsOHz6ssLAwtWvXTp988onDMQDAcH4YbGihAG/iNROKrxZ3JiQBgNv8PNgwDIXa4lcTigHAa/hRsKlqfg3DUPAGhBsA8AQ/CjbMr4G3I9wAQE3YbFJZsfT6jX4TbJhfA29HuAGA6vKjBphc5g1fQrgBgOrwkwaYVbVQYBgK3oxwAwDVUVZs+gaYzjp5SwxDwfsRbgDAHRfPsalgwgaYVXXypoUCfAHhBgBc5WyOTULKhTM2JuLsaig6ecOXEG4AwBXOOntXzLExyRe+s0nD0oVO3gQb+BLCDQBciR909q5q7RquhoIvItwAwOX4weJ8rF0DsyHcAEBVTB5sWLsGZkW4AYBL+cGqw7RQgJkRbgDgYjabtChdOvTlr9tMGGwYhoKZEW4A4GJlxY7BxkSrDjMMBX9BuAGAClar4+J8k/ZK4bGmuCKKYSj4E8INADibY5OQYqpgwzAU/AnhBoB/q2qOjQkW52MYCv6KcAPAv5l0jg3DUPBnhBsA/slZA0yTzLFhGAr+jnADwP9U1QDTx4MNw1DABYQbAP7FpA0wGYYCfkW4AeA/TNgAs6pO3gxDwZ8RbgD4BxP2ibLZbLpzfra+Onjavo1hKIBwA8AfmDTYnCwucwg2nK0BLiDcADA3kwWbimGoIfOzHebXbJvSTw3C63K2BlA1wk1paam+/PJLHTx4UCUlJWrYsKE6duyo5s2b10Z9AFB9Jgs2ziYNS1Jq0/oEG+AiLoebzZs3a+7cufrPf/6j8+fPKzo6WqGhoTp16pRKS0vVokUL3X///Ro3bpwiIyNrs2YAuDITBhtna9e8Oy6N+TXAJVz6V37rrbdq2LBhatasmT7++GOdPXtWJ0+e1E8//aSSkhL98MMPmjJlirKysnTddddp7dq1tV03AFTNRMHGZrOpuPQXh2DTPDZc305P16oJPRUeXIdgA1zCpTM3AwcO1LJlyxQUFOT08RYtWqhFixYaNWqUdu3apby8PI8WCQAucdYA04eDDWvXANVjsdlsNqOLuJoKCwsVHR2tgoICRUVFGV0OAE9xtuqwjwcbWigAv3Ln+5urpQD4PpvNeTsFH22AeWmwYe0awD0eCzejRo3SoUOH9Omnn3rqkADgmrLiX4OND686XFVvKIahAPd4LNw0btxYAT74PyQAPsxZZ+8HNkjBEcbVVE3MrwE8x2PhZsaMGZ46FABcWVWdveuGG1dTNTG/BvAs5twA8D0m6exd1TAU82uAmnE73Nxzzz2XfXzRokXVLgYArsgknb0ZhgJqj9vh5vTp0w73z58/r507d+rMmTPq27evxwoDgEpMsjgfw1BA7XI73CxfvrzSNqvVqgcffFDXXnutR4oCgEpMEGwYhgKuDo8t4rd792717t3b61cnZhE/wAeZINgwDAXUjCGL+O3bt0+//PKLpw4HABf4eLBxdrZGYhgKqE1uh5uMjAyH+zabTXl5eVq1apVGjRrlscIAwNeDTVVnaxiGAmqX2+Fmx44dDvcDAgLUsGFDvfDCC1e8kgoAXGaCYMOkYcAYboebdevW1UYdAHCBj3f2ZtIwYDwW8QPgPWw2aVG6dOjLX7f5WLC5c362vjr465IZTBoGrj6P/bZ47LHHGJYCUDNlxY7BJiHFp4LNyeIyh2DTJjGKYAMYwGNnbg4fPqxDhw556nAA/ImzBpiT9krhsV6/6nDFMNSQ+dkOE4e3TemnBuF1GYYCDOCxcPPmm2966lAA/ElVDTB9INg4uxpKklKb1ifYAAZizg0A4/hwA8yqroZ6d1waE4cBg1Ur3BQXF2v9+vXKzc1VWVmZw2MTJkzwSGEATM5HG2ByNRTg/aq1zs0tt9yikpISFRcXKyYmRidOnFBYWJji4uIINwCuzEfXsKGFAuAb3P5N8sgjj2jQoEE6ffq0QkND9cUXX+jgwYPq3Lmznn/++dqoEYCZ+HCwuenF9Q7BhquhAO/k9pmbnJwcvf766woICFBgYKBKS0vVokULzZ49W6NGjdIf/vCH2qgTgBn4eLBhGArwDW7/RgkKClLA//9FFBcXp9zcXElSdHQ0l4IDqJoPBhubzabi0l8qBZusjF4KD65DsAG8lNtnbjp27KitW7cqOTlZvXr10tSpU3XixAn9/e9/1/XXX18bNQLwdT4WbKpau4b5NYBvcPs3y4wZM5SYmChJeuaZZ1S/fn09+OCDOn78uBYsWODxAgH4OB8MNnfOz1bbaWuYXwP4KLfP3KSmptp/jouL0+rVqz1aEACT8MEGmFW1UGDtGsC3sIgfAM/zsQaYtFAAzMWl3zI333yzvvjiiyvud/bsWc2aNUuvvvpqjQsD4MN8qAFmVcNQtFAAfJdLZ26GDBmiO+64Q9HR0Ro0aJBSU1PVqFEjhYSE6PTp09q1a5c2bdqkDz/8UAMHDtRzzz1X23UD8EY+1gCTYSjAnCw2m83myo6lpaV69913tXTpUm3atEkFBQUXDmCxqE2bNkpPT9e9996r3/zmN7VacE0VFhYqOjpaBQUFioqKMrocwDyqaoD5wEavDDbOVhtmGArwXu58f7scbi5VUFCgc+fOqUGDBgoKCqpWoUYg3AC14HINML1sKMpZbyjpwjDUu+PSCDaAl3Ln+7vaE4qjo6MVHR1d3acDMAub7cIZGx9ogFlVbyhWGwbMhaulAFSfzSYVn/h1KMqLr4i6tIWCdGF+zQcP92TtGsBkCDcAqsfZHJsHfGMYirM1gLkRbgC4z9kcm6QbLgxFeZGqhqFYaRgwN0P/i7VhwwYNGjRIjRo1ksVi0YoVK674nM8++0ydOnVScHCwWrZsqcWLF9d6nQAu4qydQuZh6Z7VXjXHpmIYihYKgP+pVrg5c+aMFi5cqMzMTJ06dUqStH37dh0+fNit4xQXF6t9+/YuL/q3f/9+DRw4UH369FFOTo4mTpyosWPHas2aNW6/BwDVUFWfqOAIrwk2VXXy/nZ6ulZNYH4N4A/cHpb6+uuv1a9fP0VHR+vAgQO67777FBMTo/fff1+5ublasmSJy8caMGCABgwY4PL+8+fPV/PmzfXCCy9Ikn7zm99o06ZNeumll5Senu7uWwHgDh9ogMkwFACpGmduMjIyNHr0aP3www8KCQmxb7/lllu0YcMGjxZ3qezsbPXr189hW3p6urKzs6t8TmlpqQoLCx1uANzk7HJvLws2NlvlYMMwFOCf3D5zs3XrVr3++uuVtjdu3Fj5+fkeKaoq+fn5io+Pd9gWHx+vwsJCnTt3TqGhoZWeM3PmTE2fPr1W6wJM73yJ11/ufe58uT3YcDUU4N/c/u0UHBzs9OzHnj171LBhQ48U5UmZmZkqKCiw3w4dOmR0SYBv88LLvS/1wcM9FR5ch2AD+Cm3f0Pdeuuteuqpp3T+/HlJF3pL5ebmavLkybrjjjs8XuDFEhISdPToUYdtR48eVVRUlNOzNtKFMBYVFeVwA1ADPhAYfKBEALXI7XDzwgsvqKioSHFxcTp37px69eqlli1bKjIyUs8880xt1GiXlpamrKwsh21r165VWlparb4uAADwHW7PuYmOjtbatWu1adMmff311yoqKlKnTp0qTfR1RVFRkfbu3Wu/v3//fuXk5CgmJkZNmjRRZmamDh8+bL8Ca9y4cZo3b54effRR3XPPPfr000/1zjvvaNWqVW6/NgAAMKdqr1Dcs2dP9ezZs0Yvvm3bNvXp08d+PyMjQ5I0atQoLV68WHl5ecrNzbU/3rx5c61atUqPPPKI5s6dq2uuuUYLFy7kMnAAAGDndrh5+eWXnW63WCwKCQlRy5YtdeONNyowMPCKx+rdu7dsNluVjztbfbh3797asWOHy/UCAAD/4na4eemll3T8+HGVlJSofv36kqTTp08rLCxMEREROnbsmFq0aKF169YpKSnJ4wUDAABcjtsTimfMmKEuXbrohx9+0MmTJ3Xy5Ent2bNH3bp109y5c5Wbm6uEhAQ98sgjtVEvAADAZbl95mbKlClatmyZrr32Wvu2li1b6vnnn9cdd9yhH3/8UbNnz671y8IBAACccfvMTV5enn755ZdK23/55Rf7CsWNGjXS2bNna14dAACAm9wON3369NEDDzzgMKl3x44devDBB9W3b19J0jfffKPmzZt7rkoAAAAXuR1u3njjDcXExKhz584KDg5WcHCwUlNTFRMTozfeeEOSFBERYe/cDQAAcDW5PecmISFBa9eu1ffff689e/ZIklq1aqVWrVrZ97l47RoAAICrqdqL+LVu3VqtW7f2ZC0AAAA1Vq1w89NPP2nlypXKzc1VWVmZw2MvvviiRwoDAACoDrfDTVZWlm699Va1aNFC33//va6//nodOHBANptNnTp1qo0aAQAAXOb2hOLMzExNmjRJ33zzjUJCQrRs2TIdOnRIvXr10pAhQ2qjRgAAAJe5HW6+++47jRw5UpJUp04dnTt3ThEREXrqqac0a9YsjxcIAADgDrfDTXh4uH2eTWJiovbt22d/7MSJE56rDAAAoBrcnnNzww03aNOmTfrNb36jW265RX/5y1/0zTff6P3339cNN9xQGzUCAAC4zO1w8+KLL6qoqEiSNH36dBUVFWnp0qVKTk7mSikAAGA4t8NNixYt7D+Hh4dr/vz5Hi0IAACgJtyec9OiRQudPHmy0vYzZ844BB8AAAAjuB1uDhw4oPLy8krbS0tLdfjwYY8UBQAAUF0uD0utXLnS/vOaNWsUHR1tv19eXq6srCw1a9bMo8UBAAC4y+VwM3jwYEmSxWLRqFGjHB4LCgpSs2bN6AQOAAAM53K4sVqtkqTmzZtr69atio2NrbWiAAAAqsvtq6X2799fG3UAAAB4hEvh5uWXX3b5gBMmTKh2MQAAADXlUrh56aWXXDqYxWIh3AAAAEO5FG4YigIAAL7C7XVuLmaz2WSz2TxVCwAAQI1VK9wsWbJEKSkpCg0NVWhoqNq1a6e///3vnq4NAADAbdVqnPnEE0/ooYceUo8ePSRJmzZt0rhx43TixAk98sgjHi8SAADAVW6Hm1deeUWvvfaaRo4cad926623qm3btnryyScJNwAAwFBuD0vl5eWpe/fulbZ3795deXl5HikKAACgutwONy1bttQ777xTafvSpUuVnJzskaIAAACqy+1hqenTp2vYsGHasGGDfc7N5s2blZWV5TT0AAAAXE0un7nZuXOnJOmOO+7Ql19+qdjYWK1YsUIrVqxQbGystmzZottvv73WCgUAAHCFy2du2rVrpy5dumjs2LH64x//qH/84x+1WRcAAEC1uHzmZv369Wrbtq3+8pe/KDExUaNHj9bGjRtrszYAAAC3uRxufvvb32rRokXKy8vTK6+8ov3796tXr1667rrrNGvWLOXn59dmnQAAAC5x+2qp8PBwjRkzRuvXr9eePXs0ZMgQvfrqq2rSpIluvfXW2qgRAADAZTXqLdWyZUs99thjmjJliiIjI7Vq1SpP1QUAAFAtbl8KXmHDhg1atGiRli1bpoCAAA0dOlT33nuvJ2sDAABwm1vh5siRI1q8eLEWL16svXv3qnv37nr55Zc1dOhQhYeH11aNAAAALnM53AwYMECffPKJYmNjNXLkSN1zzz1q1apVbdYGAADgNpfDTVBQkN577z39/ve/V2BgYG3WBAAAUG0uh5uVK1fWZh0AAAAeUaOrpQAAALwN4QYAAJgK4QYAAJgK4QYAAJgK4QYAAJgK4QYAAJgK4QYAAJgK4QYAAJgK4QYAAJgK4QYAAJgK4QYAAJgK4QYAAJgK4QYAAJgK4QYAAJgK4QYAAJgK4QYAAJgK4QYAAJgK4QYAAJgK4QYAAJgK4QYAAJgK4QYAAJgK4QYAAJgK4QYAAJiKV4SbV199Vc2aNVNISIi6deumLVu2VLnv4sWLZbFYHG4hISFXsVoAAODNDA83S5cuVUZGhqZNm6bt27erffv2Sk9P17Fjx6p8TlRUlPLy8uy3gwcPXsWKAQCANzM83Lz44ou67777NGbMGLVp00bz589XWFiYFi1aVOVzLBaLEhIS7Lf4+PirWDEAAPBmhoabsrIyffXVV+rXr599W0BAgPr166fs7Owqn1dUVKSmTZsqKSlJt912m7799tsq9y0tLVVhYaHDDQAAmJeh4ebEiRMqLy+vdOYlPj5e+fn5Tp/TqlUrLVq0SP/+97/1j3/8Q1arVd27d9dPP/3kdP+ZM2cqOjrafktKSvL4+wAAAN7D8GEpd6WlpWnkyJHq0KGDevXqpffff18NGzbU66+/7nT/zMxMFRQU2G+HDh26yhUDAICrqY6RLx4bG6vAwEAdPXrUYfvRo0eVkJDg0jGCgoLUsWNH7d271+njwcHBCg4OrnGtAADANxh65qZu3brq3LmzsrKy7NusVquysrKUlpbm0jHKy8v1zTffKDExsbbKBAAAPsTQMzeSlJGRoVGjRik1NVVdu3bVnDlzVFxcrDFjxkiSRo4cqcaNG2vmzJmSpKeeeko33HCDWrZsqTNnzui5557TwYMHNXbsWCPfBgAA8BKGh5thw4bp+PHjmjp1qvLz89WhQwetXr3aPsk4NzdXAQG/nmA6ffq07rvvPuXn56t+/frq3LmzPv/8c7Vp08aotwAAALyIxWaz2Ywu4moqLCxUdHS0CgoKFBUVZXQ5gG8oK5ZmNLrw82NHpLrhxtbjREnZL2ozdY0kaddT6Qqra/j/3QB4kDvf3z53tRQAAMDlEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpEG4AAICpeEW4efXVV9WsWTOFhISoW7du2rJly2X3f/fdd9W6dWuFhIQoJSVFH3744VWqFAAAeDvDw83SpUuVkZGhadOmafv27Wrfvr3S09N17Ngxp/t//vnnGj58uO69917t2LFDgwcP1uDBg7Vz586rXDkAAPBGFpvNZjOygG7duqlLly6aN2+eJMlqtSopKUkPP/yw/ud//qfS/sOGDVNxcbE++OAD+7YbbrhBHTp00Pz586/4eoWFhYqOjlZBQYGioqI890Y8yGa16lzJWaPLAH51vkRhc1tLkkom5Up1ww0uqLKSsnKlPv2JJGnXU+kKq1vH4IoAeJI739+G/usvKyvTV199pczMTPu2gIAA9evXT9nZ2U6fk52drYyMDIdt6enpWrFihdP9S0tLVVpaar9fWFhY88Jr2bmSswp7vonRZQBOdX76E51TiNFlAECVDB2WOnHihMrLyxUfH++wPT4+Xvn5+U6fk5+f79b+M2fOVHR0tP2WlJTkmeIBP7TVep3OKdjoMi4rtWl9hQYFGl0GAAOZ/rxtZmamw5mewsJCrw84oWGRF079A16mbVCYdlksRpdxWaFBgbJ4eY0Aapeh4SY2NlaBgYE6evSow/ajR48qISHB6XMSEhLc2j84OFjBwd79P81LWQICFBYRbXQZAAD4JEOHperWravOnTsrKyvLvs1qtSorK0tpaWlOn5OWluawvyStXbu2yv0BAIB/MXxYKiMjQ6NGjVJqaqq6du2qOXPmqLi4WGPGjJEkjRw5Uo0bN9bMmTMlSX/+85/Vq1cvvfDCCxo4cKDefvttbdu2TQsWLDDybQAAAC9heLgZNmyYjh8/rqlTpyo/P18dOnTQ6tWr7ZOGc3NzFRDw6wmm7t2766233tKUKVP02GOPKTk5WStWrND1119v1FsAAABexPB1bq42X1jnBgAAOHLn+9vwFYoBAAA8iXADAABMhXADAABMhXADAABMhXADAABMhXADAABMhXADAABMhXADAABMhXADAABMhXADAABMhXADAABMhXADAABMhXADAABMhXADAABMhXADAABMhXADAABMhXADAABMhXADAABMhXADAABMhXADAABMhXADAABMhXADAABMhXADAABMpY7RBVxtNptNklRYWGhwJQAAwFUV39sV3+OX43fh5uzZs5KkpKQkgysBAADuOnv2rKKjoy+7j8XmSgQyEavVqiNHjigyMlIWi8XocqpUWFiopKQkHTp0SFFRUUaX47P4HD2Hz9Jz+Cw9g8/Rc3zhs7TZbDp79qwaNWqkgIDLz6rxuzM3AQEBuuaaa4wuw2VRUVFe+xfNl/A5eg6fpefwWXoGn6PnePtneaUzNhWYUAwAAEyFcAMAAEyFcOOlgoODNW3aNAUHBxtdik/jc/QcPkvP4bP0DD5HzzHbZ+l3E4oBAIC5ceYGAACYCuEGAACYCuEGAACYCuEGAACYCuHGC7366qtq1qyZQkJC1K1bN23ZssXoknzOhg0bNGjQIDVq1EgWi0UrVqwwuiSfNXPmTHXp0kWRkZGKi4vT4MGDtXv3bqPL8jmvvfaa2rVrZ18kLS0tTR999JHRZZnCs88+K4vFookTJxpdis958sknZbFYHG6tW7c2uqwaI9x4maVLlyojI0PTpk3T9u3b1b59e6Wnp+vYsWNGl+ZTiouL1b59e7366qtGl+Lz1q9fr/Hjx+uLL77Q2rVrdf78efXv31/FxcVGl+ZTrrnmGj377LP66quvtG3bNvXt21e33Xabvv32W6NL82lbt27V66+/rnbt2hldis9q27at8vLy7LdNmzYZXVKNcSm4l+nWrZu6dOmiefPmSbrQCyspKUkPP/yw/ud//sfg6nyTxWLR8uXLNXjwYKNLMYXjx48rLi5O69ev14033mh0OT4tJiZGzz33nO69916jS/FJRUVF6tSpk/73f/9XTz/9tDp06KA5c+YYXZZPefLJJ7VixQrl5OQYXYpHcebGi5SVlemrr75Sv3797NsCAgLUr18/ZWdnG1gZ8KuCggJJF76YUT3l5eV6++23VVxcrLS0NKPL8Vnjx4/XwIEDHX5nwn0//PCDGjVqpBYtWuiuu+5Sbm6u0SXVmN81zvRmJ06cUHl5ueLj4x22x8fH6/vvvzeoKuBXVqtVEydOVI8ePXT99dcbXY7P+eabb5SWlqaff/5ZERERWr58udq0aWN0WT7p7bff1vbt27V161ajS/Fp3bp10+LFi9WqVSvl5eVp+vTp+u1vf6udO3cqMjLS6PKqjXADwGXjx4/Xzp07TTEmb4RWrVopJydHBQUFeu+99zRq1CitX7+egOOmQ4cO6c9//rPWrl2rkJAQo8vxaQMGDLD/3K5dO3Xr1k1NmzbVO++849PDpYQbLxIbG6vAwEAdPXrUYfvRo0eVkJBgUFXABQ899JA++OADbdiwQddcc43R5fikunXrqmXLlpKkzp07a+vWrZo7d65ef/11gyvzLV999ZWOHTumTp062beVl5drw4YNmjdvnkpLSxUYGGhghb6rXr16uu6667R3716jS6kR5tx4kbp166pz587Kysqyb7NarcrKymJcHoax2Wx66KGHtHz5cn366adq3ry50SWZhtVqVWlpqdFl+JybbrpJ33zzjXJycuy31NRU3XXXXcrJySHY1EBRUZH27dunxMREo0upEc7ceJmMjAyNGjVKqamp6tq1q+bMmaPi4mKNGTPG6NJ8SlFRkcP/PPbv36+cnBzFxMSoSZMmBlbme8aPH6+33npL//73vxUZGan8/HxJUnR0tEJDQw2uzndkZmZqwIABatKkic6ePau33npLn332mdasWWN0aT4nMjKy0pyv8PBwNWjQgLlgbpo0aZIGDRqkpk2b6siRI5o2bZoCAwM1fPhwo0urEcKNlxk2bJiOHz+uqVOnKj8/Xx06dNDq1asrTTLG5W3btk19+vSx38/IyJAkjRo1SosXLzaoKt/02muvSZJ69+7tsP3//u//NHr06KtfkI86duyYRo4cqby8PEVHR6tdu3Zas2aNfve73xldGvzYTz/9pOHDh+vkyZNq2LChevbsqS+++EINGzY0urQaYZ0bAABgKsy5AQAApkK4AQAApkK4AQAApkK4AQAApkK4AQAApkK4AQAApkK4AQAApkK4AQAApkK4AXDVjR49WoMHDzbs9UeMGKEZM2Z45FhlZWVq1qyZtm3b5pHjAag5VigG4FEWi+Wyj0+bNk2PPPKIbDab6tWrd3WKush///tf9e3bVwcPHlRERIRHjjlv3jwtX77coektAOMQbgB4VEVjTUlaunSppk6dqt27d9u3RUREeCxUVMfYsWNVp04dzZ8/32PHPH36tBISErR9+3a1bdvWY8cFUD0MSwHwqISEBPstOjpaFovFYVtERESlYanevXvr4Ycf1sSJE1W/fn3Fx8frb3/7m4qLizVmzBhFRkaqZcuW+uijjxxea+fOnRowYIAiIiIUHx+vESNG6MSJE1XWVl5ervfee0+DBg1y2N6sWTPNmDFD99xzjyIjI9WkSRMtWLDA/nhZWZkeeughJSYmKiQkRE2bNtXMmTPtj9evX189evTQ22+/XcNPD4AnEG4AeIU333xTsbGx2rJlix5++GE9+OCDGjJkiLp3767t27erf//+GjFihEpKSiRJZ86cUd++fdWxY0dt27ZNq1ev1tGjRzV06NAqX+Prr79WQUGBUlNTKz32wgsvKDU1VTt27NCf/vQnPfjgg/YzTi+//LJWrlypd955R7t379Y///lPNWvWzOH5Xbt21caNGz33gQCoNsINAK/Qvn17TZkyRcnJycrMzFRISIhiY2N13333KTk5WVOnTtXJkyf19ddfS7owz6Vjx46aMWOGWrdurY4dO2rRokVat26d9uzZ4/Q1Dh48qMDAQMXFxVV67JZbbtGf/vQntWzZUpMnT1ZsbKzWrVsnScrNzVVycrJ69uyppk2bqmfPnho+fLjD8xs1aqSDBw96+FMBUB2EGwBeoV27dvafAwMD1aBBA6WkpNi3xcfHS5KOHTsm6cLE4HXr1tnn8ERERKh169aSpH379jl9jXPnzik4ONjppOeLX79iKK3itUaPHq2cnBy1atVKEyZM0Mcff1zp+aGhofazSgCMVcfoAgBAkoKCghzuWywWh20VgcRqtUqSioqKNGjQIM2aNavSsRITE52+RmxsrEpKSlRWVqa6dete8fUrXqtTp07av3+/PvroI33yyScaOnSo+vXrp/fee8++/6lTp9SwYUNX3y6AWkS4AeCTOnXqpGXLlqlZs2aqU8e1X2UdOnSQJO3atcv+s6uioqI0bNgwDRs2THfeeaduvvlmnTp1SjExMZIuTG7u2LGjW8cEUDsYlgLgk8aPH69Tp05p+PDh2rp1q/bt26c1a9ZozJgxKi8vd/qchg0bqlOnTtq0aZNbr/Xiiy/qX//6l77//nvt2bNH7777rhISEhzW6dm4caP69+9fk7cEwEMINwB8UqNGjbR582aVl5erf//+SklJ0cSJE1WvXj0FBFT9q23s2LH65z//6dZrRUZGavbs2UpNTVWXLl104MABffjhh/bXyc7OVkFBge68884avScAnsEifgD8yrlz59SqVSstXbpUaWlpHjnmsGHD1L59ez322GMeOR6AmuHMDQC/EhoaqiVLllx2sT93lJWVKSUlRY888ohHjgeg5jhzAwAATIUzNwAAwFQINwAAwFQINwAAwFQINwAAwFQINwAAwFQINwAAwFQINwAAwFQINwAAwFQINwAAwFT+H6NwcMXRlrAyAAAAAElFTkSuQmCC",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "param_entries = {'A': [(0, 0),\n",
+ " ('t_A/2', 'va', 'hold'),\n",
+ " ('t_A', 'vb', 'linear')],\n",
+ " 'B': [(0, 0),\n",
+ " ('t_B / 2', 'va', 'hold'),\n",
+ " ('t_B', 'vb', 'linear')]}\n",
+ "\n",
+ "c_pulse = TablePT(param_entries)\n",
+ "print(c_pulse.duration)\n",
+ "_ = plot(c_pulse, dict(t_A=4, t_B=5, va=1, vb=2, t_wait = 2), sample_rate=100)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "As you see channel `'A'` only was defined until 4ns and holds this level to the end of the pulse."
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/doc/source/examples/00ArithmeticWithPulseTemplates.ipynb b/doc/source/examples/00ArithmeticWithPulseTemplates.ipynb
new file mode 100644
index 000000000..1715f000e
--- /dev/null
+++ b/doc/source/examples/00ArithmeticWithPulseTemplates.ipynb
@@ -0,0 +1,246 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "collapsed": true,
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
+ "source": [
+ "# Arithmetic with Pulse Templates\n",
+ "\n",
+ "Pulse templates support some simple arithmetic operations that make it nearly a vector space. It is implemented via\n",
+ "the two classes `ArithmeticPulseTemplate` and `ArithmeticAtomicPulseTemplate`. This notebook demonstrates the direct\n",
+ "and indirect (via invoking operators) usage of these two pulse template classes. We start by defining some building\n",
+ "blocks."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import math\n",
+ "from qupulse.pulses import TablePT, FunctionPT, RepetitionPT, AtomicMultiChannelPT, plotting\n",
+ "\n",
+ "# define some building blocks\n",
+ "sin_pt = FunctionPT('sin(omega*t)', 't_duration', channel='X')\n",
+ "cos_pt = FunctionPT('sin(omega*t)', 't_duration', channel='Y')\n",
+ "exp_pt = AtomicMultiChannelPT(sin_pt, cos_pt)\n",
+ "tpt = TablePT({'X': [(0, 0), (3., 4.), ('t_duration', 2., 'linear')],\n",
+ " 'Y': [(0, 1.), ('t_y', 5.), ('t_duration', 0., 'linear')]})\n",
+ "\n",
+ "complex_pt = RepetitionPT(tpt, 5) @ exp_pt\n",
+ "\n",
+ "parameters = dict(t_duration=10, omega=math.pi*2/10, t_y=3.4)\n",
+ "_ = plotting.plot(complex_pt, parameters, show=False)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "collapsed": false,
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
+ "source": [
+ "### Operations with pulse templates and scalars\n",
+ "Operations between a pulse template and a scalar are implemented via `ArithmeticAtomicPulseTemplate`.\n",
+ "\n",
+ "#### Scale\n",
+ "Given an arbitrary pulse template $P$ and a scalar $\\alpha$ we can scale the amplitude of all channels by multiplying\n",
+ "$P$ with $\\alpha$. Multiplying with $\\alpha^{-1}$ (some people call it dividing by $\\alpha$) is also implemented. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from qupulse.expressions import Expression\n",
+ "\n",
+ "scaled = 'alpha' * complex_pt\n",
+ "\n",
+ "# casts alpha implicitly to ExpressionScalar\n",
+ "multiplied = complex_pt * 'x + y'\n",
+ "divided = complex_pt / 4.4\n",
+ "\n",
+ "_ = plotting.plot(scaled, {**parameters, 'alpha': 2}, show=False)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "collapsed": false,
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
+ "source": [
+ "#### Offset\n",
+ "You can add and subtract expression like objects to/from arbitrary pulse templates.\n",
+ " "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "offset_pt = scaled + 'offset * alpha'\n",
+ "\n",
+ "diff_pt = 4 - complex_pt"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "collapsed": false,
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
+ "source": [
+ "#### Channel specific operands\n",
+ "If you only want to apply an operation to a specific subset of a pulse template's channels you can do this by using a\n",
+ "dictionary as the other operand. Channels not in the dictionary are treated as if the dictionary contains the neutral\n",
+ "element of the operation i.e. 0 for addition and subtraction and 1 for multiplication and division."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "scaled_x = {'X': 'x_scale'} * complex_pt\n",
+ "scaled_x_y = {'X': 'x_scale', 'Y': 2} * complex_pt\n",
+ "\n",
+ "offset_x = complex_pt + {'X': 2}\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "collapsed": false,
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
+ "source": [
+ "### Adding and subtracting pulse templates\n",
+ "Addition and subtraction of pulse templates returns an atomic pulse template, the `ArithmeticAtomicPulseTemplate`\n",
+ " - Both have the same length\n",
+ " - Both are atomic. Otherwise they are interpreted as atomic.\n",
+ " - Channels defined in only one of the two operands are implicitly defined as 0 in the other"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAioAAAGwCAYAAACHJU4LAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAABAoElEQVR4nO3deXRTdfrH8U/o3tKWnbZaNilWVoGCIvxGUEZARR0VHQ4goIzKoAgdR6aIICit4AJuB9zF+Y0K6uAw4wiDDIIwgAVF4QeC7Mg6rIUWG2jz+6MmNG3SJm2Se5O8X+fk2Nzc3jxNa/Lw/T7P92ux2Ww2AQAAmFAdowMAAABwh0QFAACYFokKAAAwLRIVAABgWiQqAADAtEhUAACAaZGoAAAA04o0OoDaKC0t1cGDB5WYmCiLxWJ0OAAAwAM2m01nzpxRWlqa6tSpeswkqBOVgwcPKj093egwAABADezfv1+XXnpplecEdaKSmJgoqewHTUpKMjgaAADgiYKCAqWnpzs+x6sS1ImKfbonKSmJRAUAgCDjSdkGxbQAAMC0SFQAAIBpkagAAADTCuoaFQBA6CgpKdH58+eNDgM+EBUVpYiICJ9ci0QFAGAom82mw4cP69SpU0aHAh+qV6+eUlJSar3OGYkKAMBQ9iSlSZMmio+PZwHPIGez2VRUVKSjR49KklJTU2t1PRIVAIBhSkpKHElKw4YNjQ4HPhIXFydJOnr0qJo0aVKraSCKaQEAhrHXpMTHxxscCXzN/jutbd0RiQoAwHBM94QeX/1OSVQAAIBpkagAAADTIlEBAMDH9uzZI4vFoo0bNxodikd69+6tcePGGR2GSyQqAADAraeeekqpqak6ceKE0/HvvvtOMTEx+sc//uHX5ydRAQAAbuXk5Cg9PV1jxoxxHDt//ryGDx+uoUOH6uabb/br85OoAABMxWazqch6wZCbzWbzOM7S0lLNnDlTrVu3VkxMjJo1a6bp06c7nbNr1y716dNH8fHx6tSpk9asWeN47Pjx4xo8eLAuueQSxcfHq0OHDvrggw+cvr93794aO3asHnvsMTVo0EApKSl68sknnc6xWCx688039Zvf/Ebx8fHKyMjQokWLnM7ZvHmzBgwYoLp166pp06YaNmyYjh075tHPGRkZqffee0+ffvqpPv74Y0nS9OnTderUKc2aNcvTl6vGWPANAGAq586XqO3kJYY895Zp/RQf7dlHY05Ojt544w3NmjVLvXr10qFDh/TDDz84nfP444/rueeeU0ZGhh5//HENHjxYO3bsUGRkpH7++Wd17dpVEyZMUFJSkj777DMNGzZMl112mbp37+64xrx585Sdna1169ZpzZo1GjFihHr27Klf//rXjnOmTp2qmTNn6tlnn9XLL7+sIUOGaO/evWrQoIFOnTql6667TqNGjdKsWbN07tw5TZgwQXfddZf+/e9/e/SzZmZmKi8vT6NHj1ZiYqLy8vK0ePFiJSUlefT9tWH4iMqBAwc0dOhQNWzYUHFxcerQoYPWr19vdFgAALh15swZvfjii5o5c6aGDx+uyy67TL169dKoUaOcznv00Ud10003qU2bNpo6dar27t2rHTt2SJIuueQSPfroo7ryyivVqlUrPfzww+rfv78WLFjgdI2OHTtqypQpysjI0D333KOsrCwtW7bM6ZwRI0Zo8ODBat26tXJzc3X27Fl9/fXXkqRXXnlFnTt3Vm5urjIzM9W5c2e9/fbbWr58ubZv3+7xz/zII4+offv2uvHGGzV69Gj16dOnJi+d1wwdUTl58qR69uypPn366PPPP1fjxo31448/qn79+kaGBQAwUFxUhLZM62fYc3ti69atKi4u1vXXX1/leR07dnR8bd/z5ujRo8rMzFRJSYlyc3O1YMECHThwQFarVcXFxZVW6S1/Dft17PvouDonISFBSUlJjnO+++47LV++XHXr1q0U386dO9WmTRsPfuKyKabHH39cX375pSZNmuTR9/iCoYnKjBkzlJ6ernfeecdxrGXLlgZGBAAwmsVi8Xj6xSj2vWyqExUV5fjavlJraWmpJOnZZ5/Viy++qNmzZ6tDhw5KSEjQuHHjZLVa3V7Dfh37NTw55+zZsxo4cKBmzJhRKT5vNwyMjIx0+m8gGDr1s2jRImVlZWnQoEFq0qSJOnfurDfeeMPt+cXFxSooKHC6AQAQaBkZGYqLi6s0BeON1atX69Zbb9XQoUPVqVMntWrVyqupGE916dJF//d//6cWLVqodevWTreEhASfP5+vGZqo7Nq1S3PmzFFGRoaWLFmi0aNHa+zYsZo3b57L8/Py8pScnOy4paenBzhiAACk2NhYTZgwQY899pjee+897dy5U2vXrtVbb73l8TUyMjK0dOlS/ec//9HWrVv1wAMP6MiRIz6PdcyYMTpx4oQGDx6s/Px87dy5U0uWLNHIkSNVUlLi8+fzNUPH1kpLS5WVlaXc3FxJUufOnbV582bNnTtXw4cPr3R+Tk6OsrOzHfcLCgpIVgAAhnjiiScUGRmpyZMn6+DBg0pNTdWDDz7o8fdPmjRJu3btUr9+/RQfH6/7779ft912m06fPu3TONPS0rR69WpNmDBBN9xwg4qLi9W8eXP1799fdeoY3lNTLYvNm6ZxH2vevLl+/etf680333QcmzNnjp5++mkdOHCg2u8vKChQcnKyTp8+HZAWKQCAb/3888/avXu3WrZsqdjYWKPDgQ9V9bv15vPb0FSqZ8+e2rZtm9Ox7du3q3nz5gZFBAAAzMTQRGX8+PFau3atcnNztWPHDr3//vt6/fXXnZbpBQAA4cvQRKVbt25auHChPvjgA7Vv315PPfWUZs+erSFDhhgZFgAAMAnDG9Vvvvlmv29oBJiRzWbTufOVK+7joiIc6y0AQLgzPFEBwpHNZtOdc9dow96TlR7Lal5fHz3Yg2QFAGSCvX6AcHTufInLJEWS1u896XKkBQDCESMqgMHWT+qr+OgIFVlLlPX0F0aHAwCmQqICGCw+OsL0+5oAgFGY+gEAwMf27Nkji8WijRs3Gh2KR3r37q1x48YZHYZLJCqACRVZS1RkvaAi6wUZuHg0AEiSJkyYoBYtWujMmTNOxwcOHKhf/epXlXZz9iXGmwETKl+rQhcQAKNNmzZNn332mbKzs/XGG29Ikt5++20tX75c3333nV/3DGJEBTCJuKgIZTWvX+k4XUCAOZWWlmrmzJlq3bq1YmJi1KxZM02fPt3pnF27dqlPnz6Kj49Xp06dtGbNGsdjx48f1+DBg3XJJZcoPj5eHTp00AcffOD0/b1799bYsWP12GOPqUGDBkpJSdGTTz7pdI7FYtGbb76p3/zmN4qPj1dGRoYWLVrkdM7mzZs1YMAA1a1bV02bNtWwYcN07Ngxj3/WmJgYzZs3T/PmzdPixYu1b98+jR8/XjNnztRll13m8XVqgkQFMAmLxaKPHuyhLdP6acu0flo/qa/RIQHGsNkka6ExNy+mWnNycvTMM8/oiSee0JYtW/T++++radOmTuc8/vjjevTRR7Vx40a1adNGgwcP1oULFySVbdrXtWtXffbZZ9q8ebPuv/9+DRs2TF9//bXTNebNm6eEhAStW7dOM2fO1LRp07R06VKnc6ZOnaq77rpL33//vW688UYNGTJEJ06ckCSdOnVK1113nTp37qz169dr8eLFOnLkiO666y6vfi1du3ZVTk6ORo0apWHDhql79+4aPXq0V9eoCUN3T64tdk9GsCqyXlDbyUskSVum9XPZ9ePJOUCwc7nDrrVQyk0zJqCJB6XohGpPO3PmjBo3bqxXXnlFo0aNqvT4nj171LJlS7355pu67777JElbtmxRu3bttHXrVmVmZrq87s0336zMzEw999xzkspGVEpKSvTVV185zunevbuuu+46PfPMM5LK/pEzadIkPfXUU5KkwsJC1a1bV59//rn69++vp59+Wl999ZWWLFniuMZPP/2k9PR0bdu2TW3atFHv3r115ZVXavbs2VX+3OfPn9dll12mo0ePavv27WrWrJnbc321ezLvfAAAeGnr1q0qLi7W9ddfX+V5HTt2dHydmpoqSTp69KgyMzNVUlKi3NxcLViwQAcOHJDValVxcbHi4+PdXsN+naNHj7o9JyEhQUlJSY5zvvvuOy1fvlx169atFN/OnTvVpk0bD37iMkuXLtXhw4clSfn5+VUmKr5CogIEgSKrc40K+wEhpEXFl41sGPXcHoiLi/PsclFRjq/t/8/aO2SeffZZvfjii5o9e7Y6dOighIQEjRs3Tlar1e017Nep2GVT1Tlnz57VwIEDNWPGjErx2ZMnT5w8eVK/+93vNGnSJNlsNv3+97/Xtddeq0aNGnl8jZogUQGCQMUVa+kEQkizWDyafjFSRkaG4uLitGzZMpdTP55YvXq1br31Vg0dOlRSWQKzfft2tW3b1pehqkuXLvrkk0/UokULRUbW/GP/4YcfVkpKiiZOnChJ+tvf/qYxY8Zo/vz5vgrVJYppAZNy1wUk0QkEGC02NlYTJkzQY489pvfee087d+7U2rVr9dZbb3l8jYyMDC1dulT/+c9/tHXrVj3wwAM6cuSIz2MdM2aMTpw4ocGDBys/P187d+7UkiVLNHLkSJWUePY+snDhQn300UeaN2+eIiMjFRkZqXnz5unTTz/VJ5984vOYy2NEBTApexdQ+YSE/YAA83jiiScUGRmpyZMn6+DBg0pNTdWDDz7o8fdPmjRJu3btUr9+/RQfH6/7779ft912m06fPu3TONPS0rR69WpNmDBBN9xwg4qLi9W8eXP179/fo/VPjh07pgcffFBTpkxR+/btHcc7dOigKVOm+H0KiK4fwAA17eihEwihpqrOEAQ3X3X9MPUDAABMi3+OAUGqfCcQXUAAQhWJChCk2A8IQDhg6gcIIuwHBCDcMKICBJGKnUB0ASFUBHFfB9zw1e+URAUIMhaLhW4fhAz7iqpFRUUer/aK4FBUVCSp8qq53uLdDgBgmIiICNWrV8+xL018fDy1VkHOZrOpqKhIR48eVb169RQREVGr65GoACGC/YAQrFJSUiSp0kZ7CG716tVz/G5rg0QFCBHsB4RgZbFYlJqaqiZNmuj8+fNGhwMfiIqKqvVIih2JChDE7F1A6/eerPSYvROIehYEi4iICJ99uCF08A4GBDH2AwIQ6khUgCBHFxCAUMaCbwAAwLRIVAAAgGkxXgyEMDYuBBDsSFSAEMbGhQCCHVM/QIhh40IAoYQRFSDEsHEhgFBCogKEIFqWAYQKpn4AAIBp8U8uIIywcSGAYEOiAoQRNi4EEGyY+gFCnLsuIIlOIADmx4gKEOLYuBBAMCNRAcIAXUAAghVTPwAAwLQMTVSefPJJWSwWp1tmZqaRIQEAABMxfCy4Xbt2+uKLi3PlkZGGhwSEFTYuBGBmhmcFkZGRSklJ8ejc4uJiFRcXO+4XFBT4KywgbLBxIQAzM7xG5ccff1RaWppatWqlIUOGaN++fW7PzcvLU3JysuOWnp4ewEiB0MHGhQCChcVms9mMevLPP/9cZ8+e1eWXX65Dhw5p6tSpOnDggDZv3qzExMRK57saUUlPT9fp06eVlJQUyNARzmw26XyR87GoeMmLUYgi6wW1nbxEkrRlWj9DOnJsNpvLjQuNigdA+CgoKFBycrJHn9+GvhsNGDDA8XXHjh111VVXqXnz5lqwYIHuu+++SufHxMQoJiYmkCECzmw26e1+0v51zsfTr5buXexVsmI0WpYBBAPDp37Kq1evntq0aaMdO3YYHQrg2vmiykmKJO1fW3mUBQBQa6ZKVM6ePaudO3cqNTXV6FCA6j26o+xmZy2SrIUXb8bNqgJAyDB03PfRRx/VwIED1bx5cx08eFBTpkxRRESEBg8ebGRYgGei453vP9fa+X4QTgdJ7LAMwFwMTVR++uknDR48WMePH1fjxo3Vq1cvrV27Vo0bNzYyLMBzUfFlCcn+tZUf279WKjx2MaHxsuDWKOywDMBMDE1UPvzwQyOfHqg9i6Vs1KR8fYq16OLoSvlRFhOPsNjbldfvPVnpMXvLMoW3AIzAOw9QWxaLFJ1w8b67URZ7wW35c02CHZYBmBWJCuBrFUdZyo+wWO3HLihOP+uczNNuT7syADPiXQnwh4qjLHa/JCzxkrbGSvmlbSRbv8DGBgBBxFTtyUBIsk8FudCtznbWXwGAKjCiAvibi4LbosICxb+YaWBQ3mGHZQBGIVEBAqHiVJD1wsWvzxdJ1nL/K5qwjZkdlgEYhUQFMFilkRWTtDG7a1mmXRlAIPFOAxghKl75pW3KalQqMkkbc8WWZdqVARiBRAUwgsWiQdYpilOxNkzqWzY64aqNWTJ0KoiWZQBG4x0IMIxF5xRbNnJSMRkIkhVtAcDfaE8GzMJdG7N9KggAwhAjKoBZVLWircmwwzKAQCFRAczE3Yq21gojKga3MLPDMoBAIVEBgkHFkRUD6lbYYRmAEXhXAczK3S7MkiEtzOywDMAIJCqAWblYet/oFmbalQEEGu84gJm5q1mRaGEGEBZoTwaCCS3MAMIMIypAMDFxCzM7LAPwBxIVINiYtIWZHZYB+AOJChAqDGhhZodlAP7GuwgQzAxuYWaHZQD+RqICBLPqWpgDEgItywD8h3cXINhV1cJswForAOBLJCpAKGOtFQBBjnVUgFBjkrVWiqwlKrJecNxsNlvAnhtA6GBEBQg1JllrhR2WAfgCIypAKLLXrUQnSNHxF49biyRr4cWbj0c57O3KrthblgHAG4yoAOHEz2utsMMyAF9jRAUIde5qViS/1K3Y25Uv3iJ8en0A4YURFSDUmWCtFQCoKRIVIByw1gqAIEWiAoS7AK61wg7LALxFogKEI3d7BPl5fyB2WAbgLRIVIBwFcK0VdlgGUBu8QwDhqqq6FZ8+DTssA6g5EhUAzqwV2pV9UGDLDssAaop3DgDO/LwoHAB4gwXfAAR8UTgA8BQjKgBYFA6AaZGoAChjwKJw5ddVkVhbBUBlpklUnnnmGeXk5OiRRx7R7NmzjQ4HQHl+WhSuYvcPa6sAqMgUNSr5+fl67bXX1LFjR6NDAWDnrm6lljUr9nVVXLGvrQIAdoaPqJw9e1ZDhgzRG2+8oaefftrocADY+WlRuIrrqkisrQLAPcNHVMaMGaObbrpJffv2rfbc4uJiFRQUON0A+JG9biU6QYqO9+Fly9ZVuXiL8Nm1AYQWQ0dUPvzwQ33zzTfKz8/36Py8vDxNnTrVz1EBqJYfFoUDAFcMS1T279+vRx55REuXLlVsbKxH35OTk6Ps7GzH/YKCAqWnp/srRADusCgcgAAxLFHZsGGDjh49qi5dujiOlZSUaOXKlXrllVdUXFysiAjn4eCYmBjFxMQEOlQAkvsdlyWf7rpcvmWZdmUAhiUq119/vTZt2uR0bOTIkcrMzNSECRMqJSkADBagReHKF9XSrgzAsEQlMTFR7du3dzqWkJCghg0bVjoOwCT8tCicvWV5/d6TTsft7cpsaAiEL/7vB+AbtVgUrmLLMu3KAOxMlah8+eWXRocAwBvu6lZqULNib1kGgPJ4VwBQc35aFA4A7EhUANROVXUrAFBLJCoA/MNHi8KxwzIQ3khUAPiHjxaFY4dlILwZvtcPgBDibsdlyatdl9lhGYAdIyoAfMdHi8KxwzIAO68TleLiYq1bt0579+5VUVGRGjdurM6dO6tly5b+iA9AsPFRcS3tygAkLxKV1atX68UXX9Tf//53nT9/XsnJyYqLi9OJEydUXFysVq1a6f7779eDDz6oxMREf8YMIFjVYvVaAOHJoxqVW265RXfffbdatGihf/3rXzpz5oyOHz+un376SUVFRfrxxx81adIkLVu2TG3atNHSpUv9HTeAYPRcayk3rez2dn/JZjM6IgAm59GIyk033aRPPvlEUVFRLh9v1aqVWrVqpeHDh2vLli06dOiQT4MEEMR8uHotgPDjUaLywAMPeHzBtm3bqm3btjUOCECI8cPqteXXVmFdFSC0UakGwP98vHpt+e4f1lUBQpvP1lEZPny4rrvuOl9dDkA4sBZJ1sKLtypqVtytrcK6KkBo89mIyiWXXKI6dVg/DoAXvFi9tuLaKqyrAoQHnyUqubm5vroUgFDmrrhWqrbAlrVVgPDD//EAAstHq9cCCA9eJyr33ntvlY+//fbbNQ4GQJioqriWReEAlON1onLy5Emn++fPn9fmzZt16tQpimkB1F75kRUPd1wu364s0bIMhBKvE5WFCxdWOlZaWqrRo0frsssu80lQAMJMLReFq1hUS8syEDp80qZTp04dZWdna9asWb64HIBwY69bmXiw7Pbojmq/xV27skTLMhBKfFZMu3PnTl24cMFXlwMQbrxcFK5iu7JEyzIQirxOVLKzs53u22w2HTp0SJ999pmGDx/us8AAQJJzca3kVGBLuzIQ+rz+P/zbb791ul+nTh01btxYzz//fLUdQQDgNS8WhQMQerxOVJYvX+6POADgolosCgcgtDBmCsB8fLAoHDssA6HBZ4nKxIkTdfjwYRZ8A+AbtdxxmR2WgdDgs10EDxw4oD179vjqcgDgXvldl8vtuMwOy0Do8dmIyrx583x1KQCompvVa9lhGQg9PhtRAQC/shfYVmQvrv2FvWW57BYRwAAB+EONRlQKCwu1YsUK7du3T1ar1emxsWPH+iQwAHBSscCWHZeBsFCjdVRuvPFGFRUVqbCwUA0aNNCxY8cUHx+vJk2akKgA8J9aFtgCCD5eT/2MHz9eAwcO1MmTJxUXF6e1a9dq79696tq1q5577jl/xAgAVStfXFuhwNauyFqiIusFx83m4hwA5uP1iMrGjRv12muvqU6dOoqIiFBxcbFatWqlmTNnavjw4br99tv9EScAuOdu9dpy2GEZCE5ej6hERUWpTp2yb2vSpIn27dsnSUpOTtb+/ft9Gx0AuOOuuFZyFNiywzIQ/LweUencubPy8/OVkZGha6+9VpMnT9axY8f05z//We3bt/dHjABQmQer17LDMhD8vB5Ryc3NVWpqqiRp+vTpql+/vkaPHq3//ve/ev31130eIAC4ZS+uddziXZxSvl2ZlmUg2Hg9opKVleX4ukmTJlq8eHEVZwOAQazlRlqi4tltGQhSbEoIIDS5Wb0WQHDxaOqnf//+WrvWxXbrFZw5c0YzZszQq6++WuvAAMBrHq5eCyB4eDSiMmjQIN1xxx1KTk7WwIEDlZWVpbS0NMXGxurkyZPasmWLVq1apX/+85+66aab9Oyzz/o7bgCozMvVa4usF4ts46IiaFUGTMijROW+++7T0KFD9dFHH2n+/Pl6/fXXdfr0aUllhWpt27ZVv379lJ+fryuuuMKvAQNAldytXmuvWbFeUJx+1jnFOHX/sK4KYE4e16jExMRo6NChGjp0qCTp9OnTOnfunBo2bKioqCi/BQgAPvHLyEq8pK2xUn5pGw2yTpFUlpjY11WJj6Z0DzCTGu+enJycrJSUlFolKXPmzFHHjh2VlJSkpKQk9ejRQ59//nmNrwcATqpYFK5bne3aMul/tH5S3wAHBcAbhv7T4dJLL9UzzzyjjIwM2Ww2zZs3T7feequ+/fZbtWvXzsjQAISCahaFKxs9YV0VwMwMTVQGDhzodH/69OmaM2eO1q5dS6ICwDfYcRkIaqaZjC0pKdFHH32kwsJC9ejRw+U5xcXFKi4udtwvKCgIVHgAQpG1SNLF4loA5mN4orJp0yb16NFDP//8s+rWrauFCxeqbdu2Ls/Ny8vT1KlTAxwhgJD1XGun4tqiYud6FVqWAePVKFE5deqUPv74Y+3cuVN//OMf1aBBA33zzTdq2rSpLrnkEq+udfnll2vjxo06ffq0Pv74Yw0fPlwrVqxwmazk5OQoOzvbcb+goEDp6ek1+REAhCt7ge1+50Usu9XZriumf6ZzinUco2UZMJ7Xicr333+vvn37Kjk5WXv27NHvfvc7NWjQQH/961+1b98+vffee15dLzo6Wq1blxW2de3aVfn5+XrxxRf12muvVTo3JiZGMTEMzwKohQoFtjZroSzPZbg8lZZlwHhetydnZ2drxIgR+vHHHxUbe/FfHjfeeKNWrlxZ64BKS0ud6lAAwOfK7bpsKVdou2FSX22Z1o+WZcBEvP5nQn5+vsvRjksuuUSHDx/26lo5OTkaMGCAmjVrpjNnzuj999/Xl19+qSVLlngbFgDUWrysKntbpMAWMAuvE5WYmBiX3Tbbt29X48aNvbrW0aNHdc899+jQoUNKTk5Wx44dtWTJEv3617/2NiwAqD0Xq9fK1s/YmIAw53Wicsstt2jatGlasGCBpLK9fvbt26cJEybojjvu8Opab731lrdPDwC+5aa4ViorsC06XyTFJBsQGACpBonK888/rzvvvFNNmjTRuXPndO211+rw4cPq0aOHpk+f7o8YAcB/XKxeW1RYoPgXM8u+tpZI1guSaFcGjOB1opKcnKylS5dq1apV+v7773X27Fl16dJFfftSfAYgSFVcvfaXxESS+s1crKJfalXaNUvRR6OvIVkBAqjGPXe9evVSr169fBkLAJhCXNTF/X82xI52fJ1/uI3OWf+j+Bh2jAcCxetE5aWXXnJ53GKxKDY2Vq1bt9avfvUrRUSw0ReA4GSJTpAt/WpZXCwKR80KEFheJyqzZs3Sf//7XxUVFal+/fqSpJMnTyo+Pl5169bV0aNH1apVKy1fvpxVYwEEJ4tFlnJ1K+VrVgAEltcLvuXm5qpbt2768ccfdfz4cR0/flzbt2/XVVddpRdffFH79u1TSkqKxo8f7494ASAwyi0Kp6h4o6MBwpbXIyqTJk3SJ598ossuu8xxrHXr1nruued0xx13aNeuXZo5c6bXrcoAEBTOF0nWcm+dUfFlSQ0Av/A6UTl06JAuXLhQ6fiFCxccK9OmpaXpzJkztY8OAEym4hSQLf3qsmkikhXAL7ye+unTp48eeOABffvtt45j3377rUaPHq3rrrtOkrRp0ya1bNnSd1ECgJGi4stWqXXBsn+tbNbCAAcEhA+vR1TeeustDRs2TF27dlVUVFmL3oULF3T99dc7VpqtW7eunn/+ed9GCgAGiYuO1IyU2fq/fRf3M4tXsaN1+dz5EsWzLRDgF14nKikpKVq6dKl++OEHbd++XZJ0+eWX6/LLL3ec06dPH99FCAAGs1gs+mj0NTp3vsRxrOhsgWRfraF83Qo1K4BP1XjBt8zMTGVm0q4HIDxYLBbFR5d7y4y+uFaUU91K+tVlS/KTrAA+UaNE5aefftKiRYu0b98+Wa1Wp8deeOEFnwQGAKb2S91KtzrbnY/vX1s2wlJ+SX4ANeZ1orJs2TLdcsstatWqlX744Qe1b99ee/bskc1mU5cuXfwRIwCYj8WiQdYpilOxvnqsj+ItxSwKB/iB110/OTk5evTRR7Vp0ybFxsbqk08+0f79+3Xttddq0KBB/ogRAEzKonOKVdbMNeo64z+OozZroVT+ZrMZGCMQ3LweUdm6das++OCDsm+OjNS5c+dUt25dTZs2TbfeeqtGjx5dzRUAIPjFRUUoq3l9rd97stJjlucynA9QtwLUmNeJSkJCgqMuJTU1VTt37lS7du0kSceOHfNtdABgUhaLRR892MPRCVRUfEH5z7qoWZGoWwFqwetE5eqrr9aqVat0xRVX6MYbb9Qf/vAHbdq0SX/961919dVX+yNGADClip1A9pqVDZP6lh23FknPtTYwQiD4eZ2ovPDCCzp79qwkaerUqTp79qzmz5+vjIwMOn4AhLmympWyzQwrvL1aiy5+zVorgMe8TlRatWrl+DohIUFz5871aUAAEJLKj6xQswJ4zOuun1atWun48eOVjp86dcopiQGAcFVkLVGR9YKKbNEqufSqyifYa1YAVMvrEZU9e/aopKSk0vHi4mIdOHDAJ0EBQDDLevqLcvfGqmezeP3vfd1lOX+OmhXASx4nKosWLXJ8vWTJEiUnJzvul5SUaNmyZWrRooVPgwOAYOG+Xdmi1fvO6ZwlVvHR5aZ6rBVGVKhbAVzyOFG57bbbJJVVuQ8fPtzpsaioKLVo0YIdkwGErYrtylLZFJDz6Eo5FUdWqFsBXPI4USktLZUktWzZUvn5+WrUqJHfggKAYFRp48KKouLLEpL9ays/xlorgEte16js3r3bH3EAQOizWMpGTcoX0rLWClAljxKVl156yeMLjh07tsbBAEDIs1jcj5qw1gpQiUeJyqxZszy6mMViIVEBABeKrBdrV+KiImRxlYSw1gpQiUeJCtM9AFA75Ytqs5rX10cP9ihLVtzVrVCzAkiqQY1KebZfti53+S8DAAhz7lqW1+89qXPnS8oKbyvWrVCzAjjxemVaSXrvvffUoUMHxcXFKS4uTh07dtSf//xnX8cGAEHN3rK8ZVo/bZnWT+sn9XV34i/7AyVI0fEXj1uLJGvhxdsv/zgEwkmNNiV84okn9NBDD6lnz56SpFWrVunBBx/UsWPHNH78eJ8HCQDBqtqW5aqw1grgfaLy8ssva86cObrnnnscx2655Ra1a9dOTz75JIkKANQGa60ATrxOVA4dOqRrrrmm0vFrrrlGhw4d8klQABDqyncBSeU6gapba4UWZoQZrxOV1q1ba8GCBZo4caLT8fnz5ysjI8NngQFAKKu4tL5TJ1BVa63Qwoww43WiMnXqVN19991auXKlo0Zl9erVWrZsmRYsWODzAAEgVLjfuLBCJ1BFtDAjjHmcqGzevFnt27fXHXfcoXXr1mnWrFn69NNPJUlXXHGFvv76a3Xu3NlfcQJA0PN648KL30gLM8KWx4lKx44d1a1bN40aNUq//e1v9b//+7/+jAsAQlKNu4DcTQeVr1mRqFtByPF4HZUVK1aoXbt2+sMf/qDU1FSNGDFCX331lT9jAwBU57nWUm7axdvb/VlvBSHF40Tlf/7nf/T222/r0KFDevnll7V7925de+21atOmjWbMmKHDhw/7M04AgJ29ZsUVe90KECK8Xpk2ISFBI0eO1IoVK7R9+3YNGjRIr776qpo1a6ZbbrnFHzECQFgospaoyHpBRdYLji1KXLLXrEw8ePH26I6Lj5df0ZbRFQS5Wu3107p1a02cOFHNmzdXTk6OPvvsM1/FBQBhx+3Gha7QwowwUaO9fiRp5cqVGjFihFJSUvTHP/5Rt99+u1avXu3VNfLy8tStWzclJiaqSZMmuu2227Rt27aahgQAQcfeslyRvV3ZY+6mg5gKQpDzakTl4MGDevfdd/Xuu+9qx44duuaaa/TSSy/prrvuUkKC9338K1as0JgxY9StWzdduHBBEydO1A033KAtW7bU6HoAEGwqtix71K7s+kLuW5jpDEIQ8zhRGTBggL744gs1atRI99xzj+69915dfvnltXryxYsXO91/99131aRJE23YsEG/+tWvanVthA6bzebdvyx9zLG0OeAntdq40PlCrqeD2NwQNWGzXUx8DUxuPf4/IyoqSh9//LFuvvlmRURE+CWY06dPS5IaNGjg8vHi4mIVFxc77hcUFPglDhinYlJis0mD5q7RlkPG/a7bpib9UisgyXpB8YZFAniBzQ3hrfKJieQ8KjfxoGF/Lx4nKosWLfJnHCotLdW4cePUs2dPtW/f3uU5eXl5mjp1ql/jQGCVT0zMkJS4suVQgdpNWSJJitPP2hpbdryw+IIsulB2nFEX+IHbjQs9weaG8IbNJr3dT9q/zuhIKvHBWKNvjBkzRps3b9aqVavcnpOTk6Ps7GzH/YKCAqWnpwciPPhAbUZLnEY1AqS6+LKmf6FzinUbH8kLaqvKjQs9QWcQqlJ+BMVa5D5JSb+6LJk1iCkSlYceekj/+Mc/tHLlSl166aVuz4uJiVFMTEwAI4Ov2Gw23Tl3jTa42IytIjN96H82tpdzclVcKD1f+bzyoy52Xn+oAKrFxoWeqGpzw8JjUnS887n87YauqkZQHt1hqr8FQxMVm82mhx9+WAsXLtSXX36pli1bGhkOfKz8CEqRtcRtklIxMTHTSETlIseLX2+Y1Fe2qAS3oy7r957U8UKr4qPLarrM9HPBvGq8caFnF3ffGUTBbWhzVX/iKklJv1pKaGSq37uhicqYMWP0/vvv629/+5sSExMdy/AnJycrLi7OyNDgJW+mddZP6uv48JaC9wM8PjpSio6sNOpS/kOl/IdL+YSsYu0BUJ7PuoBcX/zidBAFt+GhuvqT8iMoJhxJMzRRmTNnjiSpd+/eTsffeecdjRgxIvABoUa8mdbJal5fDROigzIxcafih4q7oXtX00OAoSi4DV3e1J+YbASlIsOnfhCcQmFax18qDt1XNbqU1by+4qL80+6P0FR+NM4n/z9RcBv8Kk7r2GzSO/2lw5sqn2uy+hNPmKKYFsGlqhGUUJnWqa2KoywVp4fswvX1Qc15tR9QTVBwG1y8aSsOgtETV0hUUK2K9SfuRlBCcVrHV/xac4CQ5246sdZdQK5QcGt+nk7rpHSQRpb7/QRpYsk7J6pUXf1J+REURgcA//DZfkCeP6HnBbflR1mC9IPQ1EJ8WscTJCqoxNP6E0ZQgMAxbFSuuoJb6lj8JwymdTxBogIn1J8AqKRiwS11LP4TZtM6niBRCXPUnwDBrVb7AdUUdSy+wbSOR0hUwhj1J0Dwq/V+QDVFHYv3yicmVSUlFYXwtI4nSFTCDPUnQPDz635ANeFNHUvFKQspPJIXb+pNwmhaxxMkKmGE+hMgNPh1P6CaB+VZHcvhTVLeJc7HQvGD2dO9dcI1cfMCiUoYOXee+hMgVJh+bZ6KoyxVTXVUTF7M/uFdMQlx9XhV0zom31vHbEz8V47aclUoa0f9CRC6fL7Mfk1VHGV54CvPikfNNOriTcGrJ8K83qQmSFRCVHWFsvHREeb+1xiAGvP7Mvs15WpfofLJS21HXXyttkmJ2UeGggSfVCHEm0JZNsIDQktAl9n3JV+OugSSJ4kSSYlPmPQvF96iUBYIbwFfZt9fajPq4i+MjBiKRCVEUCgLwPQFtjVV3aiLv5GUGCoE/6LDA4WyAMKWq1EXhCwSlSBEoSwAbxiyzD7gI3yaBSF30zwShbIAKjNsmX3AB0hUgkTFjh47CmUBuGK6ZfaBGuKvNAhUNdXDNA8AV0y5zD5QA3zCBYGqOnqY5gHgTsh2ASGs8BdsQnT0APA30yyzD1SDRMVk6OgBEAimXWYfqKCO0QHAGR09APzFXmBbkb24FjAj/mluAnT0AAiEkFlmH2GFRMVgdPQACCQKbBFs+Gs1GB09AMyA1WthViQqAUZHDwAzYvVamBWJSgDR0QPATFi9FsGAv8AAoqMHgJmwei2CAYmKn9HRA8DMKK6F2fHX6Ud09AAIZqxeCzPgk9KP6OgBEMxYvRZmQKLiQ3T0AAh27gpsKa6FUfiL8xE6egCEAlavhdnwyekjdPQACBUU2MJM+Ev0Azp6AIQiVq+FEUhUasFd6zHTPABCEavXwgh8mtZQdTUpABAKWL0WRuOvq4ZoPQYQDli9FkYjUfEQrccAwlVVxbUsCgd/I1HxAK3HAOAai8LB3+oY+eQrV67UwIEDlZaWJovFok8//dTIcNyi9RgALrLXrVRkr1kBfMnQYYDCwkJ16tRJ9957r26//XYjQ3Hi6TSPxFCnadhs0vki778vKl7i9wd4hUXhEEiGJioDBgzQgAEDPD6/uLhYxcXFjvsFBQX+CEvnzpeo7eQlLh9jmscA1SUhNpv0Tn/p8Cbvr53SQRq5uOpkhWQGqIRF4RAoQfVXlpeXp6lTpxr2/EzzBEDFpKQ2SYgnDm+S8i6p+pzyyYy1BqM2QBhhUTj4msVms9mMDkIqy84XLlyo2267ze05rkZU0tPTdfr0aSUlJfkslopTP3b8D+cH5ROT2iYlnoyOlH/e2iZAEw9K0Qk1/34gRBRZL7gdhabAFq4UFBQoOTnZo8/voBpRiYmJUUxMjN+fhyFNP6nNaIk/pmge+KrmU0rpV5c9HwAWhYNf8ZcD/6npaImrpMQfdSIWS/UjIu6SGepWAAcWhYM/kajAP2w26e1+0v511Z9bMTExUxLgSTIDgEXh4DeGJipnz57Vjh07HPd3796tjRs3qkGDBmrWrJmBkcFrFad1rEWuk5RAjZYAMA0WhUNtGJqorF+/Xn369HHcz87OliQNHz5c7777rkFRwSPeTOs8ukOK/qWeg6QECAvu6laoWYG3DP1L6d27t0zSdARveDOtk361lNCI5AQIMywKB18hpUX1mNYBUAPu6lZYawXeIFFB1aobPWFaB4CXKo6sULeCqhi6KSFMymaTrIVlt8Jj7pMU+7ROdELZjTcZAG6428hQYjNDVI0RFTiragSl/OiJxAgKAI9Vt9YKLcxwh0QFzs67qT+hKBZALVW11gotzHCHRCXcuSqUtaP+BIAf0cIMT/BXEM6qK5SNjmdVVgB+QwszPEGiEm7Kj6C4azOW2HQPQEDQwozqkKiEEwplAQQJWphhR3tyKCvfZlxVq3HFNmNajQEYgBZmuMKISqhioTYAQYYWZrhCohKq3LUZS7QaAzAtWphREYlKKKlYKGtH/QmAIEULM/gNh4qqpnpoMwYQpKpqYaYzKDyQqISKqlaUpc0YQBBzNx1EZ1B4IFEJVqwoCyAMuZsKkpgOClX8NoMRK8oCCFN0BoUfEpVgVF1HD1M9AEIYnUHhhUQlWNDRAwAu0RkU2vjtBQM6egDALTqDQhuJihm5KpSlowcA3KIzKHSRqJgNS98DQK1U1xl0vNCq+OgIx7kkLeZGomI2LH0PALVSXWcQBbfBhUTFzCiUBYAaqTgVRMFt8OI3YwbuOnoolAUAn6DgNniRqBitupoUAIBPUHAbnOoYHUDYY48eAAg4+1SQK/aC2yLrBRVZL8hmswU4OpTHiEqgsUcPABiOgtvgQaISSOzRAwCm4U3BbfmWZvu5JC6BQaISSOzRAwCmVVXBbcU6lrapSb+MspTdJ3HxHxIVf2OPHgAIGuVHWapaOG7LoQK1m7LEcZ/pIf8hUfEn9ugBgKDlqo7FZpMGzV2jLYcKnM5lesh/SFT8iY4eAAhqrlqaPxvby6PpIUZZfINExZfo6AGAkOfp9BCjLL5hsQVxg3hBQYGSk5N1+vRpJSUlGRtMdR09Ew8y1QMAIchms7ltc66IItwy3nx+M6LiK3T0AEBY8rTNWapchFsxcbF/fzgmL+4wolIbFTt6nmtd9jUdPQAQ1iqOsrgrwnUlHEZdvPn8JlGpqaqmepjmAQBUUD55qU3iIgV/8kKiEgjWQik3rfLx9KulexczggIAqFI4j7qQqARC+USFjh4AgA+Ey6gLxbT+UFXrMYu3AQB8oGJhbvk1WyT3yUvFIl0p+Edd7EwxovLqq6/q2Wef1eHDh9WpUye9/PLL6t69e7XfF7ARFVqPAQAmEQqjLkE19TN//nzdc889mjt3rq666irNnj1bH330kbZt26YmTZpU+b0BS1Tc1aNI1KQAAAzly1oXV/yRzARVonLVVVepW7dueuWVVyRJpaWlSk9P18MPP6w//elPVX6v3xIVV9M8tB4DAIJETUddXPHHVgBBU6NitVq1YcMG5eTkOI7VqVNHffv21Zo1ayqdX1xcrOLiYsf9goKavejVOl/kfgSFehQAgMnVtNbFlfV7T+rc+ZJKex4FiqGJyrFjx1RSUqKmTZs6HW/atKl++OGHSufn5eVp6tSpgQqvMlaYBQAEoeo2V3Slqq0AAimoun5ycnKUnZ3tuF9QUKD09HTfP1FUfFmBrKvjTPMAAEKAq+SlvLioCG2Z1s/xtVEMTVQaNWqkiIgIHTlyxOn4kSNHlJKSUun8mJgYxcTE+D8wi4XpHQBAWKsukQmUOkY+eXR0tLp27aply5Y5jpWWlmrZsmXq0aOHgZEBAAAzMDxVys7O1vDhw5WVlaXu3btr9uzZKiws1MiRI40ODQAAGMzwROXuu+/Wf//7X02ePFmHDx/WlVdeqcWLF1cqsAUAAOHH8HVUasPQvX4AAECNePP5bWiNCgAAQFVIVAAAgGmRqAAAANMiUQEAAKZFogIAAEyLRAUAAJgWiQoAADAtEhUAAGBaJCoAAMC0SFQAAIBpkagAAADTIlEBAACmRaICAABMi0QFAACYFokKAAAwLRIVAABgWiQqAADAtEhUAACAaZGoAAAA0yJRAQAApkWiAgAATItEBQAAmBaJCgAAMK1IowOoDZvNJkkqKCgwOBIAAOAp++e2/XO8KkGdqJw5c0aSlJ6ebnAkAADAW2fOnFFycnKV51hsnqQzJlVaWqqDBw8qMTFRFovFp9cuKChQenq69u/fr6SkJJ9eGxfxOgcGr3Ng8DoHBq9z4PjrtbbZbDpz5ozS0tJUp07VVShBPaJSp04dXXrppX59jqSkJP5HCABe58DgdQ4MXufA4HUOHH+81tWNpNhRTAsAAEyLRAUAAJgWiYobMTExmjJlimJiYowOJaTxOgcGr3Ng8DoHBq9z4JjhtQ7qYloAABDaGFEBAACmRaICAABMi0QFAACYFokKAAAwLRIVF1599VW1aNFCsbGxuuqqq/T1118bHVLIycvLU7du3ZSYmKgmTZrotttu07Zt24wOK6Q988wzslgsGjdunNGhhKQDBw5o6NChatiwoeLi4tShQwetX7/e6LBCSklJiZ544gm1bNlScXFxuuyyy/TUU095tF8M3Fu5cqUGDhyotLQ0WSwWffrpp06P22w2TZ48WampqYqLi1Pfvn31448/Biw+EpUK5s+fr+zsbE2ZMkXffPONOnXqpH79+uno0aNGhxZSVqxYoTFjxmjt2rVaunSpzp8/rxtuuEGFhYVGhxaS8vPz9dprr6ljx45GhxKSTp48qZ49eyoqKkqff/65tmzZoueff17169c3OrSQMmPGDM2ZM0evvPKKtm7dqhkzZmjmzJl6+eWXjQ4tqBUWFqpTp0569dVXXT4+c+ZMvfTSS5o7d67WrVunhIQE9evXTz///HNgArTBSffu3W1jxoxx3C8pKbGlpaXZ8vLyDIwq9B09etQmybZixQqjQwk5Z86csWVkZNiWLl1qu/baa22PPPKI0SGFnAkTJth69epldBgh76abbrLde++9Tsduv/1225AhQwyKKPRIsi1cuNBxv7S01JaSkmJ79tlnHcdOnTpli4mJsX3wwQcBiYkRlXKsVqs2bNigvn37Oo7VqVNHffv21Zo1awyMLPSdPn1aktSgQQODIwk9Y8aM0U033eT0dw3fWrRokbKysjRo0CA1adJEnTt31htvvGF0WCHnmmuu0bJly7R9+3ZJ0nfffadVq1ZpwIABBkcWunbv3q3Dhw87vX8kJyfrqquuCtjnYlBvSuhrx44dU0lJiZo2bep0vGnTpvrhhx8Miir0lZaWaty4cerZs6fat29vdDgh5cMPP9Q333yj/Px8o0MJabt27dKcOXOUnZ2tiRMnKj8/X2PHjlV0dLSGDx9udHgh409/+pMKCgqUmZmpiIgIlZSUaPr06RoyZIjRoYWsw4cPS5LLz0X7Y/5GogLDjRkzRps3b9aqVauMDiWk7N+/X4888oiWLl2q2NhYo8MJaaWlpcrKylJubq4kqXPnztq8ebPmzp1LouJDCxYs0F/+8he9//77ateunTZu3Khx48YpLS2N1zmEMfVTTqNGjRQREaEjR444HT9y5IhSUlIMiiq0PfTQQ/rHP/6h5cuX69JLLzU6nJCyYcMGHT16VF26dFFkZKQiIyO1YsUKvfTSS4qMjFRJSYnRIYaM1NRUtW3b1unYFVdcoX379hkUUWj64x//qD/96U/67W9/qw4dOmjYsGEaP3688vLyjA4tZNk/+4z8XCRRKSc6Olpdu3bVsmXLHMdKS0u1bNky9ejRw8DIQo/NZtNDDz2khQsX6t///rdatmxpdEgh5/rrr9emTZu0ceNGxy0rK0tDhgzRxo0bFRERYXSIIaNnz56V2uu3b9+u5s2bGxRRaCoqKlKdOs4fWxERESotLTUootDXsmVLpaSkOH0uFhQUaN26dQH7XGTqp4Ls7GwNHz5cWVlZ6t69u2bPnq3CwkKNHDnS6NBCypgxY/T+++/rb3/7mxITEx1zncnJyYqLizM4utCQmJhYqeYnISFBDRs2pBbIx8aPH69rrrlGubm5uuuuu/T111/r9ddf1+uvv250aCFl4MCBmj59upo1a6Z27drp22+/1QsvvKB7773X6NCC2tmzZ7Vjxw7H/d27d2vjxo1q0KCBmjVrpnHjxunpp59WRkaGWrZsqSeeeEJpaWm67bbbAhNgQHqLgszLL79sa9asmS06OtrWvXt329q1a40OKeRIcnl75513jA4tpNGe7D9///vfbe3bt7fFxMTYMjMzba+//rrRIYWcgoIC2yOPPGJr1qyZLTY21taqVSvb448/bisuLjY6tKC2fPlyl+/Hw4cPt9lsZS3KTzzxhK1p06a2mJgY2/XXX2/btm1bwOKz2Gws6QcAAMyJGhUAAGBaJCoAAMC0SFQAAIBpkagAAADTIlEBAACmRaICAABMi0QFAACYFokKAAAwLRIVALUyYsSIwC2l7cKwYcMcuxbXltVqVYsWLbR+/XqfXA9A7bEyLQC3LBZLlY9PmTJF48ePl81mU7169QITVDnfffedrrvuOu3du1d169b1yTVfeeUVLVy40GkTNgDGIVEB4JZ9s0hJmj9/viZPnuy0S3DdunV9liDUxKhRoxQZGam5c+f67JonT55USkqKvvnmG7Vr185n1wVQM0z9AHArJSXFcUtOTpbFYnE6Vrdu3UpTP71799bDDz+scePGqX79+mratKneeOMNxy7kiYmJat26tT7//HOn59q8ebMGDBigunXrqmnTpho2bJiOHTvmNraSkhJ9/PHHGjhwoNPxFi1aKDc3V/fee68SExPVrFkzp12MrVarHnroIaWmpio2NlbNmzdXXl6e4/H69eurZ8+e+vDDD2v56gHwBRIVAD43b948NWrUSF9//bUefvhhjR49WoMGDdI111yjb775RjfccIOGDRumoqIiSdKpU6d03XXXqXPnzlq/fr0WL16sI0eO6K677nL7HN9//71Onz6trKysSo89//zzysrK0rfffqvf//73Gj16tGMk6KWXXtKiRYu0YMECbdu2TX/5y1/UokULp+/v3r27vvrqK9+9IABqjEQFgM916tRJkyZNUkZGhnJychQbG6tGjRrpd7/7nTIyMjR58mQdP35c33//vaSyupDOnTsrNzdXmZmZ6ty5s95++20tX75c27dvd/kce/fuVUREhJo0aVLpsRtvvFG///3v1bp1a02YMEGNGjXS8uXLJUn79u1TRkaGevXqpebNm6tXr14aPHiw0/enpaVp7969Pn5VANQEiQoAn+vYsaPj64iICDVs2FAdOnRwHGvatKkk6ejRo5LKimKXL1/uqHmpW7euMjMzJUk7d+50+Rznzp1TTEyMy4Lf8s9vn66yP9eIESO0ceNGXX755Ro7dqz+9a9/Vfr+uLg4x2gPAGNFGh0AgNATFRXldN9isTgdsycXpaWlkqSzZ89q4MCBmjFjRqVrpaamunyORo0aqaioSFarVdHR0dU+v/25unTpot27d+vzzz/XF198obvuukt9+/bVxx9/7Dj/xIkTaty4sac/LgA/IlEBYLguXbrok08+UYsWLRQZ6dnb0pVXXilJ2rJli+NrTyUlJenuu+/W3XffrTvvvFP9+/fXiRMn1KBBA0llhb2dO3f26poA/IOpHwCGGzNmjE6cOKHBgwcrPz9fO3fu1JIlSzRy5EiVlJS4/J7GjRurS5cuWrVqlVfP9cILL+iDDz7QDz/8oO3bt+ujjz5SSkqK0zowX331lW644Yba/EgAfIREBYDh0tLStHr1apWUlOiGG25Qhw4dNG7cONWrV0916rh/mxo1apT+8pe/ePVciYmJmjlzprKystStWzft2bNH//znPx3Ps2bNGp0+fVp33nlnrX4mAL7Bgm8Agta5c+d0+eWXa/78+erRo4dPrnn33XerU6dOmjhxok+uB6B2GFEBELTi4uL03nvvVbkwnDesVqs6dOig8ePH++R6AGqPERUAAGBajKgAAADTIlEBAACmRaICAABMi0QFAACYFokKAAAwLRIVAABgWiQqAADAtEhUAACAaZGoAAAA0/p/FEaMvyNV3twAAAAASUVORK5CYII=",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjUAAAGwCAYAAABRgJRuAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAA730lEQVR4nO3deXSU9b3H8c9kX4ksgSQa9mgKIgIRFOkVkCugYulVsBxBQEGhqIXoFYIsRYUIoiLqkbpQsddaUCqlpYCIVAEBQQHhsMmObJE1kGACydw/YsZkMklmkmeW55n365wcMtsz30xC5pPf7/v7PTa73W4XAACAyYX4uwAAAAAjEGoAAIAlEGoAAIAlEGoAAIAlEGoAAIAlEGoAAIAlEGoAAIAlhPm7AF8qLi7WsWPHFB8fL5vN5u9yAACAG+x2uy5cuKCUlBSFhFQ+HhNUoebYsWNKTU31dxkAAKAGjhw5omuuuabS24Mq1MTHx0sqeVHq1Knj52oAAIA7cnNzlZqa6ngfr0xQhZrSKac6deoQagAAMJnqWkdoFAYAAJZAqAEAAJZAqAEAAJYQVD01AADrKCoq0uXLl/1dBgwQHh6u0NDQWh+HUAMAMBW73a4TJ07o3Llz/i4FBrrqqquUlJRUq33kCDUAAFMpDTQNGzZUTEwMm6manN1uV35+vnJyciRJycnJNT4WoQYAYBpFRUWOQFO/fn1/lwODREdHS5JycnLUsGHDGk9F0SgMADCN0h6amJgYP1cCo5V+T2vTJ0WoAQCYDlNO1mPE95RQAwAALIFQAwAALIFQAwCAnx08eFA2m01btmzxdylu6dq1q0aPHu3vMiog1AAAAMM899xzSk5O1pkzZ8pdv3XrVkVGRupf//qX156bUAMAAAyTlZWl1NRUjRo1ynHd5cuXNXjwYA0cOFB33323156bUAMAMDW73a78wis+/7Db7R7VWVxcrBkzZqhly5aKjIxU48aNNXXq1HL32b9/v7p166aYmBi1bdtW69atc9x2+vRpDRgwQFdffbViYmLUpk0bffjhh+Ue37VrVz3xxBN6+umnVa9ePSUlJemPf/xjufvYbDa98847+u1vf6uYmBilpaVp8eLF5e6zfft29e7dW3FxcWrUqJEGDRqkU6dOufV1hoWF6f3339eiRYv08ccfS5KmTp2qc+fO6ZVXXnH35aoRNt8DAJjapctFajVpuc+fd8ezPRUT4f7baFZWlt5++2298sor6tKli44fP65du3aVu88zzzyjmTNnKi0tTc8884wGDBigvXv3KiwsTD/99JM6dOigsWPHqk6dOlqyZIkGDRqkFi1aqGPHjo5jzJs3T5mZmdqwYYPWrVunIUOG6NZbb9V///d/O+4zZcoUzZgxQy+++KJee+01PfDAAzp06JDq1aunc+fOqXv37ho2bJheeeUVXbp0SWPHjlX//v31+eefu/W1pqenKzs7WyNHjlR8fLyys7O1bNky1alTx+3XqyZsdk+jponl5uYqISFB58+f9/oLCwAw3k8//aQDBw6oWbNmioqKkiTlF14J+FBz4cIFJSYm6vXXX9ewYcMq3H7w4EE1a9ZM77zzjh5++OGS4+/YodatW2vnzp1KT093edy7775b6enpmjlzpqSSkZqioiKtXr3acZ+OHTuqe/fueuGFFySVjNRMmDBBzz33nCQpLy9PcXFxWrp0qXr16qXnn39eq1ev1vLlv7ymP/zwg1JTU7V7925de+216tq1q2688UbNmjWr0q/Zbrere/fu+vLLL/X4449XeV/J9fe2lLvv34zUAABMLTo8VDue7emX53XXzp07VVBQoNtvv73K+91www2Oz0vPgZSTk6P09HQVFRVp2rRpWrBggY4eParCwkIVFBRU2F257DFKj1N6XiVX94mNjVWdOnUc99m6datWrVqluLi4CvXt27dP1157rRtfcUl4euaZZ/Sf//xHEyZMcOsxtUWoAQCYms1m82gayB9Kz21UnfDwcMfnpTvsFhcXS5JefPFFvfrqq5o1a5batGmj2NhYjR49WoWFhZUeo/Q4pcdw5z4XL15Unz59NH369Ar1eXqyybCwsHL/eltg/xQAAGABaWlpio6O1sqVK11OP7lj7dq1+s1vfqOBAwdKKgk7e/bsUatWrYwsVe3bt9fChQvVtGlTn4URo7D6CQAAL4uKitLYsWP19NNP6/3339e+ffu0fv16vfvuu24fIy0tTStWrNBXX32lnTt36tFHH9XJkycNr3XUqFE6c+aMBgwYoI0bN2rfvn1avny5hg4dqqKiIsOfz0jmimAAAJjUxIkTFRYWpkmTJunYsWNKTk7WiBEj3H78hAkTtH//fvXs2VMxMTF65JFH1LdvX50/f97QOlNSUrR27VqNHTtW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kZSU5PhISEiQzWYrd11cXFyF6aeuXbvq8ccf1+jRo1W3bl01atRIb7/9tuNM9/Hx8WrZsqWWLl1a7rm2b9+u3r17Ky4uTo0aNdKgQYN06tSpSmsrKirSxx9/rD59+pS7vmnTppo2bZoeeughxcfHq3HjxuXOll1YWKjHHntMycnJioqKUpMmTZSdne24vW7durr11lv1t7/9rZavHgAjEGoA+NW8efPUoEEDff3113r88cc1cuRI9evXT507d9a3336rO+64Q4MGDVJ+fr4k6dy5c+revbvatWunTZs2admyZTp58qT69+9f6XN89913On/+vDIyMirc9tJLLykjI0ObN2/W73//e40cOdIxwjR79mwtXrxYCxYs0O7du/XBBx+oadOm5R7fsWNHrV692rgXBECNEWoA+FXbtm01YcIEpaWlKSsrS1FRUWrQoIGGDx+utLQ0TZo0SadPn9Z3330nqaSPpV27dpo2bZrS09PVrl07zZ07V6tWrdKePXtcPsehQ4cUGhqqhg0bVrjtzjvv1O9//3u1bNlSY8eOVYMGDbRq1SpJ0uHDh5WWlqYuXbqoSZMm6tKliwYMGFDu8SkpKTp06JDBrwqAmiDUAPCrG264wfF5aGio6tevrzZt2jiua9SokSQpJydHUknD76pVqxw9OnFxcUpPT5ck7du3z+VzXLp0SZGRkS6bmcs+f+mUWelzDRkyRFu2bNF1112nJ554Qp9++mmFx0dHRztGkQD4V5i/CwAQ3MLDw8tdttls5a4rDSLFxcWSpIsXL6pPnz6aPn16hWMlJye7fI4GDRooPz9fhYWFioiIqPb5S5+rffv2OnDggJYuXarPPvtM/fv3V48ePfTxxx877n/mzBklJia6++UC8CJCDQBTad++vRYuXKimTZsqLMy9X2E33nijJGnHjh2Oz91Vp04d3X///br//vt13333qVevXjpz5ozq1asnqaRpuV27dh4dE4B3MP0EwFRGjRqlM2fOaMCAAdq4caP27dun5cuXa+jQoSoqKnL5mMTERLVv315r1qzx6Llefvllffjhh9q1a5f27Nmjjz76SElJSeX22Vm9erXuuOOO2nxJAAxCqAFgKikpKVq7dq2Kiop0xx13qE2bNho9erSuuuoqhYRU/itt2LBh+uCDDzx6rvj4eM2YMUMZGRm66aabdPDgQf373/92PM+6det0/vx53XfffbX6mgAYg833AASFS5cu6brrrtP8+fN1yy23GHLM+++/X23bttX48eMNOR6A2mGkBkBQiI6O1vvvv1/lJn2eKCwsVJs2bTRmzBhDjgeg9hipAQAAlsBIDQAAsARCDQAAsARCDQAAsARCDQAAsARCDQAAsARCDQAAsARCDQAAsARCDQAAsARCDQAAsIT/B7yLwJV9sTObAAAAAElFTkSuQmCC",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "_ = plotting.plot(exp_pt + tpt, parameters, show=False)\n",
+ "_ = plotting.plot(exp_pt - tpt, parameters, show=False)\n",
+ "\n",
+ "combined = exp_pt + tpt"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "collapsed": false,
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
+ "source": [
+ "### Manual creation\n",
+ "For exact control what is needed we can use the classes directly instead of implicitly via the operators"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "C:\\Users\\Simon\\AppData\\Local\\Temp\\ipykernel_11640\\2452374967.py:6: ImplicitAtomicityInArithmeticPT: ArithmeticAtomicPulseTemplate treats all operands as if they are atomic. You can silence this warning by passing `silent_atomic=True` or by ignoring this category.\n",
+ " complex_added_1 = ArithmeticAtomicPT(complex_pt, '+', complex_pt)\n"
+ ]
+ }
+ ],
+ "source": [
+ "from qupulse.pulses import ArithmeticPT, ArithmeticAtomicPT\n",
+ "\n",
+ "scaled_x = ArithmeticPT({'X': 'x_scale'}, '*', complex_pt, identifier='scaled_x')\n",
+ "\n",
+ "# this raises a warning because complex_pt is treated as atomic\n",
+ "complex_added_1 = ArithmeticAtomicPT(complex_pt, '+', complex_pt)\n",
+ "\n",
+ "# this raises a warning because complex_pt is treated as atomic\n",
+ "complex_added_2 = ArithmeticAtomicPT(complex_pt, '+', complex_pt, silent_atomic=True)"
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/doc/source/examples/00ComposedPulses.ipynb b/doc/source/examples/00ComposedPulses.ipynb
new file mode 100644
index 000000000..014eb4e53
--- /dev/null
+++ b/doc/source/examples/00ComposedPulses.ipynb
@@ -0,0 +1,240 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Combining Pulse Templates\n",
+ "\n",
+ "So far we have seen how to define simple pulses using the `TablePulseTemplate` ([Modelling a Simple TablePulseTemplate](00SimpleTablePulse.ipynb)), `FunctionPulseTemplate` ([Modelling Pulses Using Functions And Expressions](00FunctionPulse.ipynb)) and `PointPulseTemplate` ([The PointPulseTemplate](00PointPulse.ipynb)) classes. These are the elementary building blocks to create pulses and we call them *atomic* pulse templates.\n",
+ "\n",
+ "We will now have a look at how to compose more complex pulse structures.\n",
+ "\n",
+ "## SequencePulseTemplate: Putting Pulses in a Sequence\n",
+ "\n",
+ "As the name suggests `SequencePulseTemplate` allows us to define a pulse as a sequence of already existing pulse templates which are run one after another. In the following example we have two templates created using `PointPulseTemplate` and want to define a higher-level pulse template that puts them in sequence."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "sequence parameters: {'v_0', 'v_1', 't_2', 't'}\n",
+ "sequence measurements: {'M'}\n"
+ ]
+ }
+ ],
+ "source": [
+ "from qupulse.pulses import PointPT, SequencePT\n",
+ "# create our atomic \"low-level\" PointPTs\n",
+ "first_point_pt = PointPT([(0, 'v_0'),\n",
+ " (1, 'v_1', 'linear'),\n",
+ " ('t', 'v_0+v_1', 'jump')],\n",
+ " channel_names=('A',),\n",
+ " measurements=[('M', 1, 't-1')])\n",
+ "second_point_pt = PointPT([(0, 'v_0+v_1'),\n",
+ " ('t_2', 'v_0', 'linear')],\n",
+ " channel_names=('A',),\n",
+ " measurements=[('M', 0, 1)])\n",
+ "\n",
+ "# define the SequencePT\n",
+ "sequence_pt = SequencePT(first_point_pt, second_point_pt)\n",
+ "\n",
+ "print(\"sequence parameters: {}\".format(sequence_pt.parameter_names))\n",
+ "print(\"sequence measurements: {}\".format(sequence_pt.measurement_names))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "It is important to note that all of the pulse templates used to create a `SequencePT` (we call those *subtemplates*) are defined on the same channels, in this case the channel `A` (otherwise we would encounter an exception). The `SequencePT` will also be defined on the same channel.\n",
+ "\n",
+ "The `SequencePT` will further have the union of all parameters defined in its subtemplates as its own parameter set. If two subtemplates defined parameters with the same name, they will be treated as the same parameters in the `SequencePT`.\n",
+ "\n",
+ "Finally, `SequencePT` will also expose all measurements defined in subtemplates. It is also possible to define additional measurements in the constructor of `SequencePT`. See [Definition of Measurements](01Measurements.ipynb) for me info about measurements.\n",
+ "\n",
+ "There are several cases where the above constraints represent a problem: Subtemplates might not all be defined on the same channel, subtemplates might define parameters with the same name which should still be treated as different parameters in the sequence or names of measurements defined by different subtemplates might collide. To deal with these, we can wrap a subtemplate with the `MappingPulseTemplate` class which allows us to rename parameters, channels and measurements or even derive parameter values from other parameters using mathematical expressions. You can learn how to do all this in [Mapping with the MappingPulseTemplate](00MappingTemplate.ipynb).\n",
+ "\n",
+ "In our example above, however, we were taking care not to encounter these problems yet. Let's plot all of them with some parameters to see the results."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkIAAAGwCAYAAABFFQqPAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAA3fUlEQVR4nO3dfVxUZf7/8feAOIDAKCkCird4f0OY2qKWN5mmLq27rbqWgqlttmip3RjV2tpusrqaXys3t9LMXVtNTWu7M/IG0kXTkk2zNBXFXNDUBAEXFc7vjx7MLxJwBmcYhvN6Ph7zeDBnrnPmc3EaeXeu6zpjMQzDEAAAgAn5eLoAAAAATyEIAQAA0yIIAQAA0yIIAQAA0yIIAQAA0yIIAQAA0yIIAQAA06rn6QJqWmlpqf773/8qODhYFovF0+UAAAAHGIahCxcuKDIyUj4+rruOY7og9N///ldRUVGeLgMAAFTDiRMn1Lx5c5cdz3RBKDg4WNIPv8iQkBAPVwMAAByRn5+vqKgo+99xVzFdECobDgsJCSEIAQDgZVw9rYXJ0gAAwLQIQgAAwLQIQgAAwLRMN0fIUSUlJbp8+bKny4AL+Pn5ydfX19NlAABqIYLQTxiGodzcXJ0/f97TpcCFGjZsqPDwcO4dBQAohyD0E2UhKCwsTIGBgfzh9HKGYaioqEinT5+WJEVERHi4IgBAbUIQ+pGSkhJ7CLrhhhs8XQ5cJCAgQJJ0+vRphYWFMUwGALBjsvSPlM0JCgwM9HAlcLWyc8q8LwDAjxGEKsBwWN3DOQUAVIQgBAAATIsgBAAATIsgZALHjh2TxWJRZmamp0txyIABAzR9+nRPlwEAMAGCELzWxYsXFRoaqsaNG6u4uNjT5QAAvBBBCF5r/fr16tKlizp27KiNGzd6uhwAgBciCF2DYRgqunTFIw/DMByus7S0VPPnz1d0dLSsVqtatGihZ599tlybo0ePauDAgQoMDFRMTIwyMjLsr509e1Zjx45Vs2bNFBgYqG7duumf//xnuf0HDBigBx98UI899phCQ0MVHh6uP/zhD+XaWCwWvfrqq/rlL3+pwMBAtWvXTu+88065Nvv379ewYcMUFBSkpk2bavz48Tpz5ozDfS2zbNkyjRs3TuPGjdOyZcuc3h8AAG6oeA0XL5eo8+xNHnnvA88MVWB9x05RcnKyXnnlFS1atEj9+vVTTk6Ovv7663JtnnzySS1YsEDt2rXTk08+qbFjx+rw4cOqV6+e/ve//+mmm27SrFmzFBISovfee0/jx49X27Zt1bt3b/sxXn/9dc2cOVO7du1SRkaGJkyYoL59++r222+3t5kzZ47mz5+vv/zlL3rhhRd0zz336Pjx4woNDdX58+c1aNAgTZ48WYsWLdLFixc1a9YsjR49Wlu2bHH4d3PkyBFlZGTorbfekmEYmjFjho4fP66WLVs6fAwAALgiVAdcuHBBixcv1vz585WYmKi2bduqX79+mjx5crl2jzzyiEaMGKH27dtrzpw5On78uA4fPixJatasmR555BHdeOONatOmjaZNm6Y77rhDb775ZrljdO/eXU8//bTatWunhIQE9ezZU5s3by7XZsKECRo7dqyio6M1d+5cFRQU6NNPP5Ukvfjii4qNjdXcuXPVsWNHxcbGavny5dq6dasOHTrkcJ+XL1+uYcOGqVGjRgoNDdXQoUP12muvVefXBwAwMa4IXUOAn68OPDPUY+/tiK+++krFxcW67bbbqmzXvXt3+89l37l1+vRpdezYUSUlJZo7d67efPNNnTx5UpcuXVJxcfFVd9n+8THKjlP2PV4VtWnQoIFCQkLsbf7zn/9o69atCgoKuqq+I0eOqH379tfsb0lJiV5//XUtXrzYvm3cuHF65JFHNHv2bPn4kO8BAI4hCF2DxWJxeHjKU8q+S+ta/Pz87D+X3Wm5tLRUkvSXv/xFixcv1v/93/+pW7duatCggaZPn65Lly5Veoyy45Qdw5E2BQUFio+P17x5866qz9EvRN20aZNOnjypMWPGlNteUlKizZs3lxumAwCgKrX7Lzwc0q5dOwUEBGjz5s1XDYc5aseOHfrFL36hcePGSfohIB06dEidO3d2Zanq0aOH1q9fr1atWqlever957ds2TL95je/0ZNPPllu+7PPPqtly5YRhAAADmMMoQ7w9/fXrFmz9Nhjj2nlypU6cuSIdu7c6dRKqnbt2ik1NVX//ve/9dVXX+n+++/XqVOnXF5rUlKSzp07p7Fjx2r37t06cuSINm3apHvvvVclJSXX3P+7777Tv/71LyUmJqpr167lHgkJCdq4caPOnTvn8roBAHUTQaiO+P3vf6+HH35Ys2fPVqdOnTRmzJir5u5U5amnnlKPHj00dOhQDRgwQOHh4Ro5cqTL64yMjNSOHTtUUlKiIUOGqFu3bpo+fboaNmzo0NyelStXqkGDBhXOh7rtttsUEBCgf/zjHy6vGwBQN1kMZ25WUwfk5+fLZrMpLy9PISEh5V773//+p6ysLLVu3Vr+/v4eqhDuwLkFAO9W1d/v68EVIQAAYFoeDUIpKSnq1auXgoODFRYWppEjR+rgwYMO77969WpZLBa3DOEAAIC6z6NBKC0tTUlJSdq5c6dSU1N1+fJlDRkyRIWFhdfc99ixY3rkkUd0yy231EClAACgLvLo8vkPP/yw3PMVK1YoLCxMn332mW699dZK9yspKdE999yjOXPm6JNPPtH58+ddWpfJpk2ZgtnOqWEYunj52qvwAHivAD9f+z3hUH216j5CeXl5kqTQ0NAq2z3zzDMKCwvTpEmT9Mknn1TZtri4WMXFxfbn+fn5lbYtuxFgUVGRwzcphHcoKiqSdPXNHusiwzD066UZ+uz4954uBYAbOfN9lKhcrfkNlpaWavr06erbt6+6du1aabvt27dr2bJlyszMdOi4KSkpmjNnjkNtfX191bBhQ/uy88DAQNK2lzMMQ0VFRTp9+rQaNmwoX1/HvrbEm128XEIIAgAH1ZoglJSUpP3792v79u2Vtrlw4YLGjx+vV155RY0bN3bouMnJyZo5c6b9eX5+vqKioiptHx4eLklO3YMHtV/Dhg3t59ZM9jw1WIH16374A8zI0e+jRNVqRRCaOnWq3n33XaWnp6t58+aVtjty5IiOHTum+Ph4+7ay77CqV6+eDh48qLZt25bbx2q1ymq1OlyLxWJRRESEwsLCdPnyZSd7gtrIz8/PFFeCKhJY35dL5wBQBY/+C2kYhqZNm6YNGzZo27Ztat26dZXtO3bsqH379pXb9tRTT+nChQtavHhxlVd6nOXr62vaP54AAJiFR4NQUlKS3njjDb399tsKDg5Wbm6uJMlms9knKyckJKhZs2ZKSUmRv7//VfOHGjZsKElVzisCAACoiEeD0EsvvSRJGjBgQLntr732miZMmCBJys7Odug7qAAAAJzl8aGxa9m2bVuVr69YscI1xQAAANPhUgsAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtjwahlJQU9erVS8HBwQoLC9PIkSN18ODBKvd55ZVXdMstt6hRo0Zq1KiRBg8erE8//bSGKgYAAHWJR4NQWlqakpKStHPnTqWmpury5csaMmSICgsLK91n27ZtGjt2rLZu3aqMjAxFRUVpyJAhOnnyZA1WDgAA6gKLYRiGp4so89133yksLExpaWm69dZbHdqnpKREjRo10osvvqiEhIRrts/Pz5fNZlNeXp5CQkKut2Sg1im6dEWdZ2+SJB14ZqgC69fzcEUAcP3c9fe7Vv0LmZeXJ0kKDQ11eJ+ioiJdvny50n2Ki4tVXFxsf56fn399RQIAgDqj1kyWLi0t1fTp09W3b1917drV4f1mzZqlyMhIDR48uMLXU1JSZLPZ7I+oqChXlQwAALxcrQlCSUlJ2r9/v1avXu3wPn/+85+1evVqbdiwQf7+/hW2SU5OVl5env1x4sQJV5UMAAC8XK0YGps6dareffddpaenq3nz5g7ts2DBAv35z3/Wxx9/rO7du1fazmq1ymq1uqpUAABQh3g0CBmGoWnTpmnDhg3atm2bWrdu7dB+8+fP17PPPqtNmzapZ8+ebq4SAADUVR4NQklJSXrjjTf09ttvKzg4WLm5uZIkm82mgIAASVJCQoKaNWumlJQUSdK8efM0e/ZsvfHGG2rVqpV9n6CgIAUFBXmmIwAAwCt5dI7QSy+9pLy8PA0YMEARERH2x5o1a+xtsrOzlZOTU26fS5cu6de//nW5fRYsWOCJLgAAAC/m8aGxa9m2bVu558eOHXNPMQAAwHRqzaoxAACAmkYQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQ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ALiB00Fo2bJlCg0N1U033SSr1Sqr1aqePXsqNDRUy5YtkyQFBQVp4cKFLi8WAADAlZyeIxQeHq7U1FR9/fXXOnTokCSpQ4cO6tChg73NwIEDXVchAACAm1T7hoodO3ZUx44dXVkLAABAjapWEPr222/1zjvvKDs7W5cuXSr32nPPPeeSwgAAANzN6SC0efNm3XnnnWrTpo2+/vprde3aVceOHZNhGOrRo4c7agQAAHALpydLJycn65FHHtG+ffvk7++v9evX68SJE+rfv79GjRrljhoBAADcwukg9NVXXykhIUGSVK9ePV28eFFBQUF65plnNG/ePKeOlZ6ervj4eEVGRspisWjjxo3X3Ke4uFhPPvmkWrZsKavVqlatWmn58uXOdgMAAMD5obEGDRrY5wVFREToyJEj6tKliyTpzJkzTh2rsLBQMTExmjhxon71q185tM/o0aN16tQpLVu2TNHR0crJyVFpaalznQAAAFA1gtDPfvYzbd++XZ06ddLw4cP18MMPa9++fXrrrbf0s5/9zKljDRs2TMOGDXO4/Ycffqi0tDQdPXpUoaGhkqRWrVo59Z4AAABlnB4ae+6553TzzTdLkubMmaPbbrtNa9asUatWrew3VHSXd955Rz179tT8+fPVrFkztW/fXo888oguXrxY6T7FxcXKz88v9wAAAJCqcUWoTZs29p8bNGigpUuXurSgqhw9elTbt2+Xv7+/NmzYoDNnzuh3v/udzp49q9dee63CfVJSUjRnzpwaqxEAAHgPp68ItWnTRmfPnr1q+/nz58uFJHcoLS2VxWLRqlWr1Lt3bw0fPlzPPfecXn/99UqvCiUnJysvL8/+OHHihFtrBAAA3sPpK0LHjh1TSUnJVduLi4t18uRJlxRVmYiICDVr1kw2m82+rVOnTjIMQ99++63atWt31T5l34cGAADwUw4HoXfeecf+86ZNm8qFkZKSEm3evNntE5f79u2rtWvXqqCgQEFBQZKkQ4cOycfHR82bN3frewMAgLrH4SA0cuRISZLFYlFiYmK51/z8/NSqVSunv3G+oKBAhw8ftj/PyspSZmamQkND1aJFCyUnJ+vkyZNauXKlJOnuu+/WH//4R917772aM2eOzpw5o0cffVQTJ05UQECAU+8NAADgcBAqu1dP69attXv3bjVu3Pi633zPnj3lvql+5syZkqTE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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkIAAAGwCAYAAABFFQqPAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAA+lElEQVR4nO3dfVhUdf7/8dcMCCgCSYhAoWKa5k2GYUVSWpHmXev+WjW3JCvbNNTMbtn8arSlq5v1LXPX3Vaz+9RKc7XN9S4NQ4uSTbM0lcQMvEEFARcUzu8PvzMLAjKDcz/Px3XNdcnhnJk3R5x5eT6f8/6YDMMwBAAA4IfM7i4AAADAXQhCAADAbxGEAACA3yIIAQAAv0UQAgAAfosgBAAA/BZBCAAA+K1AdxfgatXV1frll18UFhYmk8nk7nIAAIANDMPQyZMnFRcXJ7PZcddx/C4I/fLLL4qPj3d3GQAAoAkOHDigSy+91GHP53dBKCwsTNLZExkeHu7magAAgC1KSkoUHx9v/Rx3FL8LQpbhsPDwcIIQAABextHTWpgsDQAA/BZBCAAA+C2CEAAA8Ft+N0cIAOA7qqurVVlZ6e4y4CBBQUEOvTXeFgQhAIBXqqysVF5enqqrq91dChzEbDYrISFBQUFBLntNghAAwOsYhqGCggIFBAQoPj7e5VcR4HiWhscFBQVq27aty5oeE4QAAF7nzJkzKi8vV1xcnFq0aOHucuAgrVu31i+//KIzZ86oWbNmLnlNIjQAwOtUVVVJkkuHUOB8lr9Py9+vKxCEAABeizUjfYs7/j4JQgAAwG8RhAAAgN8iCAEA4AF++uknmUwm5ebmursUm/Tr10+TJ092dxkXjCAEAACc5tSpU4qMjFRUVJQqKircXU4dBCEAAOA0H374obp166YuXbpo+fLl7i6nDoIQAMDrGYah8sozbnkYhmFzndXV1Zo9e7Y6duyo4OBgtW3bVs8//3ytffbt26ebbrpJLVq0UM+ePZWdnW39XlFRkUaNGqVLLrlELVq0UI8ePfTee+/VOr5fv36aNGmSnnjiCUVGRiomJkbPPPNMrX1MJpP+/ve/69e//rVatGihTp06acWKFbX22bFjhwYOHKiWLVuqTZs2Gj16tI4ePWrzz2qxYMEC3X333br77ru1YMECu493NhoqAgC83qnTVeo6bbVbXnvnswPUIsi2j9OMjAy99tpreumll5SSkqKCggL98MMPtfZ5+umn9cILL6hTp056+umnNWrUKO3Zs0eBgYH6z3/+o6uvvlpPPvmkwsPDtWrVKo0ePVqXXXaZrrnmGutzvPHGG5oyZYq2bt2q7OxsjRkzRn369NGtt95q3SczM1OzZ8/Wn/70J82dO1d33XWX9u/fr8jISJ04cUI333yzxo4dq5deekmnTp3Sk08+qREjRmj9+vU2n5u9e/cqOztbH330kQzD0COPPKL9+/erXbt2Nj+Hs3FFCAAAFzh58qRefvllzZ49W/fcc48uu+wypaSkaOzYsbX2e+yxxzR48GBdfvnlyszM1P79+7Vnzx5J0iWXXKLHHntMV111lTp06KCJEyfqtttu05IlS2o9x5VXXqnp06erU6dOSktLU1JSktatW1drnzFjxmjUqFHq2LGjZsyYodLSUn355ZeSpFdffVWJiYmaMWOGunTposTERC1cuFAbNmzQ7t27bf6ZFy5cqIEDB6pVq1aKjIzUgAED9Prrrzfl9DkNV4QAAF6vebMA7Xx2gNte2xbff/+9KioqdMstt5x3vyuvvNL659jYWEnS4cOH1aVLF1VVVWnGjBlasmSJDh48qMrKSlVUVNRZZqTmc1ie5/Dhww3uExoaqvDwcOs+//73v7Vhwwa1bNmyTn179+7V5Zdf3ujPW1VVpTfeeEMvv/yyddvdd9+txx57TNOmTfOY9eEIQgAAr2cymWwennKX5s2b27RfzTW2LJ2Wq6urJUl/+tOf9PLLL+t///d/1aNHD4WGhmry5MmqrKxs8Dksz2N5Dlv2KS0t1dChQzVr1qw69VnCWWNWr16tgwcPauTIkbW2V1VVad26dbWG6dzJs39rAADwEZ06dVLz5s21bt26OsNhttq8ebN+9atf6e6775Z0NiDt3r1bXbt2dWSp6tWrlz788EO1b99egYFNiwoLFizQnXfeqaeffrrW9ueff14LFizwmCDkGdelAADwcSEhIXryySf1xBNP6M0339TevXu1ZcsWu+6k6tSpk9asWaMvvvhC33//vR588EEdOnTI4bWmp6fr2LFjGjVqlL766ivt3btXq1ev1r333mvTgqhHjhzRP/7xD91zzz3q3r17rUdaWpqWL1+uY8eOObzupiAIAQDgIv/zP/+jRx99VNOmTdMVV1yhkSNH1pm7cz5T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nDihm2++WYmJicrJydGnn36qQ4cOacSIEQ2+xrfffqvi4mIlJSXV+d6cOXOUlJSkbdu26aGHHtL48eOtV7JeeeUVrVixQkuWLNGuXbv0zjvvqH379rWOv+aaa/T555877oQAcAmCEACP0LNnT02dOlWdOnVSRkaGQkJCFBUVpQceeECdOnXStGnTVFRUpG+//VbS2Xk5iYmJmjFjhrp06aLExEQtXLhQGzZs0O7du+t9jf379ysgIEDR0dF1vjdo0CA99NBD6tixo5588klFRUVpw4YNkqT8/Hx16tRJKSkpateunVJSUjRq1Khax8fFxWn//v0OPisAnI0gBMAjXHnlldY/BwQE6OKLL1aPHj2s29q0aSNJOnz4sKSzk543bNhgnXPUsmVLdenSRZK0d+/eel/j1KlTCg4OrndCd83XtwznWV5rzJgxys3NVefOnTVp0iT961//qnN88+bNrVerAHiPQHcXAACS1KxZs1pfm0ymWtss4aW6ulqSVFpaqqFDh2rWrFl1nis2Nrbe14iKilJ5ebkqKysVFBTU6OtbXqtXr17Ky8vTP//5T61du1YjRoxQamqqPvjgA+v+x44dU+vWrW39cQF4CIIQAK/Uq1cvffjhh2rfvr0CA217K7vqqqskSTt37rT+2Vbh4eEaOXKkRo4cqd/85je67bbbdOzYMUVGRko6O3E7MTHRrucE4H4MjQHwSunp6Tp27JhGjRqlr776Snv37tXq1at17733qqqqqt5jWrdurV69eikrK8uu13rxxRf13nvv6YcfftDu3bu1dOlSxcTE1OqD9Pnnn6t///4X8iMBcAOCEACvFBcXp82bN6uqqkr9+/dXjx49NHnyZF100UUymxt+axs7dqzeeecdu14rLCxMs2fPVlJSknr37q2ffvpJn3zyifV1srOzVVxcrN/85jcX9DMBcD0aKgLwK6dOnVLnzp21ePFiJScnO+Q5R44cqZ49e+r3v/+9Q54PgOtwRQiAX2nevLnefPPN8zZetEdlZaV69OihRx55xCHPB8C1uCIEAAD8FleEAACA3yIIAQAAv0UQAgAAfosgBAAA/BZBCAAA+C2CEAAA8FsEIQAA4LcIQgAAwG8RhAAAgN/6/87odjf0xUZaAAAAAElFTkSuQmCC",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "image/png": "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b266sk8XWRr7hQTU+Pd3eDi7/+bfeOMNnTp1SsXFxWrYsKEk6dy5cwoJCVGDBg108uRJtWzZUps2bVJcXJzDCwYAwJp5cbF5eop1xb7N7gXFM2bMUPfu3fXTTz/pzJkzOnPmjA4cOKAePXrozTffVE5OjqKjozVp0iRn1AsAQJUqzkKxc8q32T1yM2XKFH322We68cYbLcdatWql2bNn68EHH9ShQ4c0a9YstoUDAOqUubHf3twCy86p0ECmpnyR3SM3ubm5unTp0hXHL126ZOlQHBsbqwsXLtS+OgAAbGR9zyluy+C77A43d999t5588knt2rXLcmzXrl16+umn1bdvX0nS7t27FR8f77gqAQCwQcW+N9yWwXfZHW4WLVqkyMhI3XLLLQoMDFRgYKC6deumyMhILVq0SJLUoEEDzZkzx+HFAgBwNdyWAVIN1txER0dr/fr12rdvnw4cOCBJatu2rdq2bWs55+6773ZchQAA2MF659SwhZn0vfExNV5p1a5dO7Vr186RtQAA4BAVG/vR98b31Ohv+pdfftHnn3+unJwclZWVVfrc3LlzHVIYAAA1ZX1bBpbd+Ba7w82GDRt0//33q2XLltq3b586duyow4cPy2QyqWvXrs6oEQAAu1WcheKeU77F7gXFqampmjx5snbv3q2goCB99tlnOnr0qO666y4NGzbMGTUCAGA3c98biZ1TvsbucPPjjz8qKSlJklSvXj2VlJSoQYMGevnllzVz5ky7nmvLli1KTExUbGysDAaDVq9efc1rSktL9eKLL6p58+YKDAxUixYttHjxYnu/DACAl2PnlO+yO9yEhoZa1tnExMTo4MGDls+dPn3arucqKipSly5dNH/+fJuvefjhh7VhwwYtWrRI+/fv1yeffFJppxYAAGbmnVNmNPbzDXavubntttuUkZGhm266SYMHD9azzz6r3bt3a+XKlbrtttvseq5BgwZp0KBBNp+/du1apaen69ChQ4qMjJQktWjRwq7XBAD4loo7p8zTU2wN9252j9zMnTtXPXr0kCRNnz5d/fr107Jly9SiRQtLEz9n+fzzz9WtWzfNmjVLN9xwg9q0aaPJkyerpKSk2mtKS0tVUFBQ6QEA8B1MT/keu0duWrZsaflzaGioFi5c6NCCrubQoUPKyMhQUFCQVq1apdOnT+u3v/2tzpw5o/fee6/K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RVXw73//W3379tWRI0fUoEEDhzzn22+/rVWrVlW66S0A1yHcAHAo8401JWnZsmWaOnWq9u/fbznWoEEDh4WKmhg7dqzq1aunhQsXOuw5z507p+joaH333Xfq0KGDw54XQM0wLQXAoaKjoy2PiIgIGQyGSscaNGhwxbRUnz59NH78eE2cOFENGzZUkyZN9O6776qoqEhjxoxRWFiYWrVqpX/84x+VXmvPnj0aNGiQGjRooCZNmuixxx7T6dOnq62tvLxcK1asUGJiYqXjLVq00IwZM/T4448rLCxMzZo10zvvvGP5fFlZmcaNG6eYmBgFBQWpefPmSktLs3y+YcOG6tWrl5YuXVrLdw+AIxBuALiFDz74QI0aNdL27ds1fvx4Pf300xo2bJhuv/12fffddxowYIAee+wxFRcXS5LOnz+vvn37KiEhQTt37tTatWt14sQJPfzww9W+xvfff6/8/Hx169btis/NmTNH3bp1065du/Tb3/5WTz/9tGXE6a233tLnn3+u5cuXa//+/fr444/VokWLStffeuut+vrrrx33hgCoMcINALfQpUsXTZkyRa1bt1ZqaqqCgoLUqFEj/eY3v1Hr1q01depUnTlzRt9//72ky+tcEhISNGPGDLVr104JCQlavHixNm3apAMHDlT5GkeOHJG/v7+ioqKu+NzgwYP129/+Vq1atdLzzz+vRo0aadOmTZKknJwctW7dWr1791bz5s3Vu3dvjRgxotL1sbGxOnLkiIPfFQA1QbgB4BY6d+5s+bO/v7+uv/56derUyXKsSZMmkqSTJ09KurwweNOmTZY1PA0aNFC7du0kSQcPHqzyNUpKShQYGFjloueKr2+eSjO/1ujRo5WVlaW2bdtqwoQJ+uqrr664Pjg42DKqBMC16rm6AACQpPr161f62GAwVDpmDiRGo1GSVFhYqMTERM2cOfOK54qJianyNRo1aqTi4mKVlZUpICDgmq9vfq2uXbsqOztb//jHP/TPf/5TDz/8sPr3768VK1ZYzj979qwaN25s65cLwIkINwA8UteuXfXZZ5+pRYsWqlfPtm9lN998syRp7969lj/bKjw8XMOHD9fw4cP10EMP6d5779XZs2cVGRkp6fLi5oSEBLueE4BzMC0FwCMlJyfr7NmzGjFihHbs2KGDBw9q3bp1GjNmjMrLy6u8pnHjxuratasyMjLseq25c+fqk08+0b59+3TgwAF9+umnio6OrtSn5+uvv9aAAQNq8yUBcBDCDQCPFBsbq61bt6q8vFwDBgxQp06dNHHiRF133XXy86v+W9vYsWP18ccf2/VaYWFhmjVrlrp166bu3bvr8OHD+vLLLy2vk5mZqfz8fD300EO1+poAOAZN/AD4lJKSErVt21bLli1Tz549HfKcw4cPV5cuXfTCCy845PkA1A4jNwB8SnBwsD788MOrNvuzR1lZmTp16qRJkyY55PkA1B4jNwAAwKswcgMAALwK4QYAAHgVwg0AAPAqhBsAAOBVCDcAAMCrEG4AAIBXIdwAAACvQrgBAABehXADAAC8yv8Ddq4hfisp6sYAAAAASUVORK5CYII=",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from qupulse.pulses.plotting import plot\n",
+ "\n",
+ "parameters = dict(t=3,\n",
+ " t_2=2,\n",
+ " v_0=1,\n",
+ " v_1=1.4)\n",
+ "\n",
+ "_ = plot(first_point_pt, parameters, sample_rate=100)\n",
+ "_ = plot(second_point_pt, parameters, sample_rate=100)\n",
+ "_ = plot(sequence_pt, parameters, sample_rate=100)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## RepetitionPulseTemplate: Repeating a Pulse\n",
+ "\n",
+ "If we simply want to repeat some pulse template a fixed number of times, we can make use of the `RepetitionPulseTemplate`. In the following, we will reuse one of our `PointPT`s, `first_point_pt` and use it to create a new pulse template that repeats it `n_rep` times, where `n_rep` will be a parameter."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "repetition parameters: {'t', 'v_0', 'v_1', 'n_rep'}\n",
+ "repetition measurements: {'M'}\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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oZo/f76WLl2qxMREt7abOHGinnnmGS1atEitWrXy8ZQAAKA8sbTcpKWlaebMmfrwww8VGRmp/fv3S5KioqIUHh4uSerXr5/q1KmjjIwMSdKECRM0duxYzZw5UwkJCa5tIiIiFBERYc0LAQAAfsPSc26mTp2q3NxcdejQQbGxsa7b7NmzXetkZ2crJyen1DanT5/WXXfdVWqb559/3oqXAAAA/Izlb0tdztKlS0vd37Vrl2+GAQAAtuA3n5YCAADwBsoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFcoNAACwFUvLTUZGhlq3bq3IyEjFxMSoR48e2rJly2W3e//999W4cWOFhYWpWbNm+vTTTw1MCwAAygNLy82yZcuUlpam1atXKzMzU0VFRerSpYsKCgouus2///1v9enTR/fdd582btyoHj16qEePHtq0aZPByQEAgL8KsXLnn3/+ean706dPV0xMjNavX6+bb775gttMnjxZt99+u0aPHi1J+utf/6rMzEy9+uqreu2113w+MwAA8G9+dc5Nbm6uJCk6Ovqi66xatUqdO3cu9VjXrl21atWqC65fWFiovLy8UjcAAGBfflNuSkpKNHz4cLVv315Nmza96Hr79+9XrVq1Sj1Wq1Yt7d+//4LrZ2RkKCoqynWLj4/36twAAMC/+E25SUtL06ZNmzRr1iyvPm96erpyc3Ndtz179nj1+QEAgH+x9Jybc4YMGaKPP/5Yy5cvV926dS+5bu3atXXgwIFSjx04cEC1a9e+4PqhoaEKDQ312qwAAMC/WXrkxul0asiQIZo/f76++uorJSYmXnabtm3bavHixaUey8zMVNu2bX01JgAAKEcsPXKTlpammTNn6sMPP1RkZKTrvJmoqCiFh4dLkvr166c6deooIyNDkvTQQw/plltu0aRJk9StWzfNmjVL69at07Rp0yx7HQAAwH9YeuRm6tSpys3NVYcOHRQbG+u6zZ4927VOdna2cnJyXPfbtWunmTNnatq0aWrRooU++OADLViw4JInIQMAgMBh6ZEbp9N52XWWLl163mM9e/ZUz549fTARAAAo7/zm01IAAADeQLkBAAC2QrkBAAC2QrkBAAC2QrkBAAC2QrkBAAC2QrkBAAC2QrkBAAC2QrkBAAC2QrkBAAC2QrkBAAC2QrkBAAC2QrkBAAC2QrkBAAC2QrkBAAC2EuLpBoWFhfrmm2+0e/dunThxQjVr1lRycrISExN9MR8AAIBH3C43K1eu1OTJk/XRRx+pqKhIUVFRCg8P15EjR1RYWKgGDRroT3/6kx544AFFRkb6cmYAAICLcuttqe7du6t3795KSEjQF198ofz8fB0+fFg///yzTpw4oZ9++kmPP/64Fi9erKuvvlqZmZm+nhsAAOCC3Dpy061bN82dO1cVKlS44M8bNGigBg0aqH///tq8ebNycnK8OiQAAIC73Co3999/v9tPeO211+raa68t80AAAABXgk9LAQAAW/Fauenfv786derkracDAAAoE48/Cn4xderUUVAQB4IAAIC1vFZuxo8f762nAgAAKDMOtQAAAFvx+MjNoEGDLvnzt956q8zDAAAAXCmPy83Ro0dL3S8qKtKmTZt07NgxTigGAACW87jczJ8//7zHSkpK9OCDD+qqq67yylAAAABl5ZVzboKCgjRy5Ei9+OKL3ng6AACAMvPaCcXbt2/XmTNnvPV0AAAAZeLx21IjR44sdd/pdConJ0effPKJ+vfv77XBAAAAysLjcrNx48ZS94OCglSzZk1NmjTpsp+kAgAA8DWPy82SJUt8MQcAAIBXWPolfsuXL1dqaqri4uLkcDi0YMGCy24zY8YMtWjRQpUqVVJsbKwGDRqkw4cP+35YAABQLnit3Dz66KMevy1VUFCgFi1aaMqUKW6tv3LlSvXr10/33Xef/vOf/+j999/XmjVr9Mc//rEsIwMAABvy2rWl9u7dqz179ni0TUpKilJSUtxef9WqVUpISNCwYcMkSYmJibr//vs1YcIEj/YLAADsy2tHbt555x199dVX3nq6C2rbtq327NmjTz/9VE6nUwcOHNAHH3ygO+6446LbFBYWKi8vr9QNAADYV7m6cGb79u01Y8YM9e7dWxUrVlTt2rUVFRV1ybe1MjIyFBUV5brFx8cbnBgAAJhWprelCgoKtGzZMmVnZ+v06dOlfnbuLSNf2Lx5sx566CGNHTtWXbt2VU5OjkaPHq0HHnhAb7755gW3SU9PL/XdPHl5eRQcAABsrEzfc3PHHXfoxIkTKigoUHR0tA4dOqRKlSopJibGp+UmIyND7du31+jRoyVJzZs3V+XKlXXTTTfpb3/7m2JjY8/bJjQ0VKGhoT6bCQAA+BeP35YaMWKEUlNTdfToUYWHh2v16tXavXu3rrvuOj3//PO+mNHlxIkTCgoqPXJwcLCks9+UDAAA4HG5ycrK0qhRoxQUFKTg4GAVFhYqPj5eEydO1KOPPurRcx0/flxZWVnKysqSJO3cuVNZWVnKzs6WdPYtpX79+rnWT01N1bx58zR16lTt2LFDK1eu1LBhw9SmTRvFxcV5+lIAAIANefy2VIUKFVxHT2JiYpSdna1rrrlGUVFRHn8UfN26derYsaPr/rlzY/r376/p06crJyfHVXQkacCAAcrPz9err76qUaNGqWrVqurUqRMfBQcAAC4el5vk5GStXbtWSUlJuuWWWzR27FgdOnRI//znP9W0aVOPnqtDhw6XfDtp+vTp5z02dOhQDR061NOxAQBAgPD4banx48e7Ttx95plnVK1aNT344IP65ZdfNG3aNK8PCAAA4AmPj9y0atXKtRwTE6PPP//cqwMBAOCukpKS876SxB3FRYWqExmsShWDderUKR9M5p/79ncVK1Y874NDZeG1yy8AAGDS6dOntXPnTpWUlHi87ZniEj3VMUZBjrMfZjHJyn37u6CgICUmJqpixYpX9DxulZvbb79dTz31lG644YZLrpefn6+///3vioiIUFpa2hUNBgDAxTidTuXk5Cg4OFjx8fEe/7/900XFKjlcoCCHQ4m1In00pf/t25+VlJRo3759ysnJUb169eRwOMr8XG6Vm549e+rOO+9UVFSUUlNT1apVK8XFxSksLExHjx7V5s2btWLFCn366afq1q2bnnvuuTIPBADA5Zw5c0YnTpxQXFycKlWq5PH2juBiOUKKFORwKCwszAcT+ue+/V3NmjW1b98+nTlzRhUqVCjz87hVbu677z7de++9ev/99zV79mxNmzZNubm5kiSHw6Frr71WXbt21dq1a3XNNdeUeRgAANxRXFwsSVf89gX8y7nfZ3Fxse/LjXT2Mgb33nuv7r33XklSbm6uTp48qerVq1/RAAAAlNWVvHUB/+Ot32eZTyg+d5VtAAAAf3Lln7cCAADwI5QbAAD8wK5du+RwOFzXW/R3HTp00PDhw60e44IoNwAAwGdOnjyp6Oho1ahRQ4WFhUb2SbkBAAA+M3fuXDVp0kSNGzfWggULjOyzTOXm2LFjeuONN5Senq4jR45IkjZs2KC9e/d6dTgAANzhdDp14vQZj26niop1qqjY4+1+fbvUBaB/raSkRJOef07/d2NLJTeIUb169fTMM8+UWmfHjh3q2LGjKlWqpBYtWmjVqlWunx0+fFh9+vRRnTp1VKlSJTVr1kzvvfdeqe07dOigYcOG6S9/+Yuio6NVu3ZtPfXUU6XWcTgceuONN/S73/1OlSpVUlJSkhYuXFhqnU2bNiklJUURERGqVauW+vbtq0OHDrn9Ws958803XZ+2fvPNNz3eviw8/rTUd999p86dOysqKkq7du3SH//4R0VHR2vevHnKzs7Wu+++64s5AQC4qJNFxbp27CJL9r356a6qVNG9f07T09P1+uuva+QTz+i6Nm0V6TyuH3/8sdQ6jz32mJ5//nklJSXpscceU58+fbRt2zaFhITo1KlTuu666zRmzBhVqVJFn3zyifr27aurrrpKbdq0cT3HO++8o5EjR+qbb77RqlWrNGDAALVv31633Xaba51x48Zp4sSJeu655/TKK6/onnvu0e7duxUdHa1jx46pU6dOGjx4sF588UWdPHlSY8aMUa9evfTVV1+5nc327du1atUqzZs3T06nUyNGjNDu3btVv359t5+jLDw+cjNy5EgNGDBAP/30U6lvVrzjjju0fPlyrw4HAIBd5Ofna/LkyXom41l179lH9RISdeONN2rw4MGl1nv44YfVrVs3XX311Ro3bpx2796tbdu2SZLq1Kmjhx9+WL/5zW/UoEEDDR06VLfffrvmzJlT6jmaN2+uJ598UklJSerXr59atWqlxYsXl1pnwIAB6tOnjxo2bKjx48fr+PHjWrNmjSTp1VdfVXJyssaPH6/GjRsrOTlZb731lpYsWaKtW7e6/ZrfeustpaSkqFq1aoqOjlbXrl319ttvlyU+j3h85Gbt2rX6xz/+cd7jderU0f79+70yFAAAngivEKzNT3d1e/3ComL9dPC4gh0OXRNX5Yr37Y4ffvhBhYWF6tixky51HfPmzZu7lmNjYyVJBw8eVOPGjVVc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+ylX5Wb//v2qVatWqcdq1aqlvLw8nTx58oLbZGRkKCoqynWLj48v8/4dDik0JEjx0eEa1bVRmZ8H7qtbLVzX1a9G5gaRuXlkbh6Z25ulb0uZkJ6erpEjR7ru5+Xllbng1K9eWVv+5v7baLhyIcFBmvtgO6vHCChkbh6Zm0fm9lauyk3t2rV14MCBUo8dOHBAVapUUXh4+AW3CQ0NVWhoqInxAACAHyhXb0u1bdtWixcvLvVYZmam2rZta9FEAADA31habo4fP66srCxlZWVJOvtR76ysLGVnZ0s6+5ZSv379XOs/8MAD2rFjh/7yl7/oxx9/1N///nfNmTNHI0aMsGJ8AADghywtN+vWrVNycrKSk5MlSSNHjlRycrLGjh0rScrJyXEVHUlKTEzUJ598oszMTLVo0UKTJk3SG2+8YeRj4AAAoHxwOJ1Op9VDmJSXl6eoqCjl5uaqSpUqVo8DAADc4Mm/3+XqnBsAAIDLodwAAABbodwAAABbodwAAABbodwAAABbodwAAABbodwAAABbodwAAABbodwAAABbodwAAABbodwAAABbodwAAABbodwAAABbodwAAABbodwAAABbodwAAABbodwAAABbodwAAABbodwAAABbodwAAABbodwAAABbodwAAABbodwAAABbCbF6ANOcTqckKS8vz+JJAACAu879u33u3/FLCbhyk5+fL0mKj4+3eBIAAOCp/Px8RUVFXXIdh9OdCmQjJSUl2rdvnyIjI+VwODzePi8vT/Hx8dqzZ4+qVKnigwn9E6+b1x0IeN287kBQXl+30+lUfn6+4uLiFBR06bNqAu7ITVBQkOrWrXvFz1OlSpVy9T8Kb+F1BxZed2DhdQeW8vi6L3fE5hxOKAYAALZCuQEAALZCufFQaGionnzySYWGhlo9ilG8bl53IOB187oDQSC87oA7oRgAANgbR24AAICtUG4AAICtUG4AAICtUG4AAICtUG48MGXKFCUkJCgsLEzXX3+91qxZY/VIPpeRkaHWrVsrMjJSMTEx6tGjh7Zs2WL1WEY9++yzcjgcGj58uNWj+NzevXt17733qnr16goPD1ezZs20bt06q8fyqeLiYj3xxBNKTExUeHi4rrrqKv31r3916/o15c3y5cuVmpqquLg4ORwOLViwoNTPnU6nxo4dq9jYWIWHh6tz58766aefrBnWiy71uouKijRmzBg1a9ZMlStXVlxcnPr166d9+/ZZN7CXXO73/b8eeOABORwOvfTSS8bm8yXKjZtmz56tkSNH6sknn9SGDRvUokULde3aVQcPHrR6NJ9atmyZ0tLStHr1amVmZqqoqEhdunRRQUGB1aMZsXbtWv3jH/9Q8+bNrR7F544ePar27durQoUK+uyzz7R582ZNmjRJ1apVs3o0n5owYYKmTp2qV199VT/88IMmTJigiRMn6pVXXrF6NK8rKChQixYtNGXKlAv+fOLEiXr55Zf12muv6ZtvvlHlypXVtWtXnTp1yvCk3nWp133ixAlt2LBBTzzxhDZs2KB58+Zpy5Yt6t69uwWTetflft/nzJ8/X6tXr1ZcXJyhyQxwwi1t2rRxpqWlue4XFxc74+LinBkZGRZOZd7BgwedkpzLli2zehSfy8/PdyYlJTkzMzOdt9xyi/Ohhx6yeiSfGjNmjPPGG2+0egzjunXr5hw0aFCpx37/+98777nnHosmMkOSc/78+a77JSUlztq1azufe+4512PHjh1zhoaGOt977z0LJvSNX7/uC1mzZo1TknP37t1mhjLgYq/7559/dtapU8e5adMmZ/369Z0vvvii8dl8gSM3bjh9+rTWr1+vzp07ux4LCgpS586dtWrVKgsnMy83N1eSFB0dbfEkvpeWlqZu3bqV+r3b2cKFC9WqVSv17NlTMTExSk5O1uuvv271WD7Xrl07LV68WFu3bpUkffvtt1qxYoVSUlIsnsysnTt3av/+/aX+9x4VFaXrr78+IP/OORwOVa1a1epRfKqkpER9+/bV6NGj1aRJE6vH8aqAu3BmWRw6dEjFxcWqVatWqcdr1aqlH3/80aKpzCspKdHw4cPVvn17NW3a1OpxfGrWrFnasGGD1q5da/UoxuzYsUNTp07VyJEj9eijj2rt2rUaNmyYKlasqP79+1s9ns888sgjysvLU+PGjRUcHKzi4mI988wzuueee6wezaj9+/dL0gX/zp37WSA4deqUxowZoz59+pS7i0p6asKECQoJCdGwYcOsHsXrKDdwW1pamjZt2qQVK1ZYPYpP7dmzRw899JAyMzMVFhZm9TjGlJSUqFWrVho/frwkKTk5WZs2bdJrr71m63IzZ84czZgxQzNnzlSTJk2UlZWl4cOHKy4uztavG+crKipSr1695HQ6NXXqVKvH8an169dr8uTJ2rBhgxwOh9XjeB1vS7mhRo0aCg4O1oEDB0o9fuDAAdWuXduiqcwaMmSIPv74Yy1ZskR169a1ehyfWr9+vQ4ePKiWLVsqJCREISEhWrZsmV5++WWFhISouLjY6hF9IjY2Vtdee22px6655hplZ2dbNJEZo0eP1iOPPKK7775bzZo1U9++fTVixAhlZGRYPZpR5/6WBerfuXPFZvfu3crMzLT9UZuvv/5aBw8eVL169Vx/53bv3q1Ro0YpISHB6vGuGOXGDRUrVtR1112nxYsXux4rKSnR4sWL1bZtWwsn8z2n06khQ4Zo/vz5+uqrr5SYmGj1SD5366236vvvv1dWVpbr1qpVK91zzz3KyspScHCw1SP6RPv27c/7mP/WrVtVv359iyYy48SJEwoKKv2nMDg4WCUlJRZNZI3ExETVrl271N+5vLw8ffPNN7b/O3eu2Pz000/68ssvVb16datH8rm+ffvqu+++K/V3Li4uTqNHj9aiRYusHu+K8baUm0aOHKn+/furVatWatOmjV566SUVFBRo4MCBVo/mU2lpaZo5c6Y+/PBDRUZGut57j4qKUnh4uMXT+UZkZOR55xRVrlxZ1atXt/W5RiNGjFC7du00fvx49erVS2vWrNG0adM0bdo0q0fzqdTUVD3zzDOqV6+emjRpoo0bN+qFF17QoEGDrB7N644fP65t27a57u/cuVNZWVmKjo5WvXr1NHz4cP3tb39TUlKSEhMT9cQTTyguLk49evSwbmgvuNTrjo2N1V133aUNGzbo448/VnFxsevvXHR0tCpWrGjV2Ffscr/vX5e4ChUqqHbt2mrUqJHpUb3P6o9rlSevvPKKs169es6KFSs627Rp41y9erXVI/mcpAve3n77batHMyoQPgrudDqdH330kbNp06bO0NBQZ+PGjZ3Tpk2zeiSfy8vLcz700EPOevXqOcPCwpwNGjRwPvbYY87CwkKrR/O6JUuWXPC/5/79+zudzrMfB3/iiSectWrVcoaGhjpvvfVW55YtW6wd2gsu9bp37tx50b9zS5YssXr0K3K53/ev2emj4A6n04ZfwwkAAAIW59wAAABbodwAAABbodwAAABbodwAAABbodwAAABbodwAAABbodwAAABbodwAAABbodwAMG7AgAGWfqV/3759XVc/v1KnT59WQkKC1q1b55XnA3Dl+IZiAF7lcDgu+fMnn3xSI0aMkNPpVNWqVc0M9T++/fZbderUSbt371ZERIRXnvPVV1/V/PnzS110EoB1KDcAvOrcRQclafbs2Ro7dmypq41HRER4rVSUxeDBgxUSEqLXXnvNa8959OhR1a5dWxs2bFCTJk289rwAyoa3pQB4Ve3atV23qKgoORyOUo9FRESc97ZUhw4dNHToUA0fPlzVqlVTrVq19Prrr6ugoEADBw5UZGSkGjZsqM8++6zUvjZt2qSUlBRFRESoVq1a6tu3rw4dOnTR2YqLi/XBBx8oNTW11OMJCQkaP368Bg0apMjISNWrV6/U1dBPnz6tIUOGKDY2VmFhYapfv74yMjJcP69WrZrat2+vWbNmXWF6ALyBcgPAL7zzzjuqUaOG1qxZo6FDh+rBBx9Uz5491a5dO23YsEFdunRR3759deLECUnSsWPH1KlTJyUnJ2vdunX6/PPPdeDAAfXq1eui+/juu++Um5urVq1anfezSZMmqVWrVtq4caP+/Oc/68EHH3QdcXr55Ze1cOFCzZkzR1u2bNGMGTOUkJBQavs2bdro66+/9l4gAMqMcgPAL7Ro0UKPP/64kpKSlJ6errCwMNWoUUN//OMflZSUpLFjx+rw4cP67rvvJJ09zyU5OVnjx49X48aNlZycrLfeektLlizR1q1bL7iP3bt3Kzg4WDExMef97I477tCf//xnNWzYUGPGjFGNGjW0ZMkSSVJ2draSkpJ04403qn79+rrxxhvVp0+fUtvHxcVp9+7dXk4FQFlQbgD4hebNm7uWg4ODVb16dTVr1sz1WK1atSRJBw8elHT2xOAlS5a4zuGJiIhQ48aNJUnbt2+/4D5Onjyp0NDQC570/L/7P/dW2rl9DRgwQFlZWWrUqJGGDRumL7744rztw8PDXUeVAFgrxOoBAECSKlSoUOq+w+Eo9di5QlJSUiJJOn78uFJTUzVhwoTznis2NvaC+6hRo4ZOnDih06dPq2LFipfd/7l9tWzZUjt37tRnn32mL7/8Ur169VLnzp31wQcfuNY/cuSIatas6e7LBeBDlBsA5VLLli01d+5cJSQkKCTEvT9lv/nNbyRJmzdvdi27q0qVKurdu7d69+6tu+66S7fffruOHDmi6OhoSWdPbk5OTvboOQH4Bm9LASiX0tLSdOTIEfXp00dr167V9u3btWjRIg0cOFDFxcUX3KZmzZpq2bKlVqxY4dG+XnjhBb333nv68ccftXXrVr3//vuqXbt2qe/p+frrr9WlS5creUkAvIRyA6BciouL08qVK1VcXKwuXbqoWbNmGj58uKpWraqgoIv/aRs8eLBmzJjh0b4iIyM1ceJEtWrVSq1bt9auXbv06aefuvazatUq5ebm6q677rqi1wTAO/gSPwAB5eTJk2rUqJFmz56ttm3beuU5e/furRYtWujRRx/1yvMBuDIcuQEQUMLDw/Xuu+9e8sv+PHH69Gk1a9ZMI0aM8MrzAbhyHLkBAAC2wpEbAABgK5QbAABgK5QbAABgK5QbAABgK5QbAABgK5QbAABgK5QbAABgK5QbAABgK5QbAABgK/8Pk8sYW3mqBqIAAAAASUVORK5CYII=",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from qupulse.pulses import RepetitionPT\n",
+ "\n",
+ "repetition_pt = RepetitionPT(first_point_pt, 'n_rep')\n",
+ "\n",
+ "print(\"repetition parameters: {}\".format(repetition_pt.parameter_names))\n",
+ "print(\"repetition measurements: {}\".format(repetition_pt.measurement_names))\n",
+ "\n",
+ "# let's plot to see the results\n",
+ "parameters['n_rep'] = 5 # add a value for our n_rep parameter\n",
+ "_ = plot(repetition_pt, parameters, sample_rate=100)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The same remarks that were made about `SequencePT` also hold for `RepetitionPT`: it will expose all parameters and measurements defined by its subtemplate and will be defined on the same channels.\n",
+ "\n",
+ "## ForLoopPulseTemplate: Repeat a Pulse with a Varying Loop Parameter\n",
+ "\n",
+ "The `RepetitionPT` simple repeats the exact same subtemplate a given number of times. Sometimes, however, it is rather required to vary the parameters of a subtemplate in a loop, for example when trying to determine the best value for a parameter of a given pulse. This is what the `ForLoopPulseTemplate` is intended for. As the name suggests, its behavior mimics that for `for-loop` constructs in programming languages by repeating its content - the subtemplate - for a number of times while at the same time supplying a loop parameter that iterates over a range of values.\n",
+ "\n",
+ "In the following we make use of this to vary the value of parameter `t` in `first_point_pt` over several iterations. More specifically, we will have all a `first_point_pt` pulse for all even values of `t` between `t_start` and `t_end` which are new parameters. For the plot we will set them to `t_start = 4` and `t_end = 13`, i.e., `t = 4, 6, 8, 10, 12`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "for loop parameters: {'v_0', 'v_1', 't_end', 't_start'}\n",
+ "for loop measurements: {'M'}\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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oDo8TnReeOEFvfzyyxo7dqxr39VXX62uXbvqscceI9EBAAB1hsdNV9nZ2br44ovP2X/xxRcrOzvbK0EBAAB4g8eJTvv27fXee++ds3/u3LkVLqoJAABgFo+brqZOnarRo0dr2bJlrj46K1as0KJFiypMgAAAAMzido3O5s2bJUnXXnutvv32WzVv3lwLFy7UwoUL1bx5c61evVq/+93vfBYoAACAp9yu0enRo4f69Omj2267TX/84x/1n//8x5dxAQAA1JjbNTpLly5V165ddf/99ysqKkrjx4/XN99848vYAAAAasTtROfSSy/VG2+8oezsbL3wwgvKzMzU5Zdfrg4dOuipp57S/v37fRknAACAxzweddW4cWPdfPPNWrp0qbZt26brrrtOL730ktq0aaOrr77aFzECAABUS43Wumrfvr0efvhhPfLIIwoNDdWnn37qrbgAAABqzOPh5U7Lli3TG2+8ofnz58vPz0+jRo3Srbfe6s3YAAAAasSjROfnn39WWlqa0tLStH37dl188cV6/vnnNWrUKDVu3NhXMQIAAFSL24lOUlKSvvrqKzVv3lxjx47VLbfcoo4dO/oyNgAAgBpxO9Fp2LCh3n//ff2///f/1KBBA1/GBAAA4BVuJzofffSRL+MAAADwuhqNugIAAKjLSHQAAIBtkegAAADbItEBAAC2RaIDAABsi0QHAADYFokOAACwLRIdAABgWyQ6AADAtkh0AACAbZHoAAAA2yLRAQAAtmVqorNs2TKNGDFC0dHRcjgcWrhwodvnrlixQv7+/rrooot8Fh8AALA2UxOdgoICJSQk6KWXXvLovGPHjmns2LEaPHiwjyIDAAB24G/mmyclJSkpKcnj8+68805df/31atCggUe1QAAAoH6xXB+dN998Uzt37tSjjz7q1vFFRUXKzc0t9/CGUsPwynVQd1HG5jtxssTsEOBjxylj+JilEp2ffvpJf/nLX/Sf//xH/v7uVUalpqYqLCzM9YiJifFKLF/+cECSdb8MD+UXSZJKLBp/bfjv5v2SrFvGdrA+65gkysDONu87/eOTzyL4imUSnZKSEl1//fWaOnWqOnTo4PZ5KSkpysnJcT327NnjlXh+zD79x/lLXrFXrlfb1u0+Kknis6VymYcKJEmHC6xZxnYS3NDUVnb4iFHmA6hJUEMTI4GdWebTIy8vT2vX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NdeYEk9tOHjwoCHJWLp0qWEYp8u0YcOGxrx581zH/Pjjj4YkY+XKlWaF6TW/vl/DMIzLL7/cuPfee80LqhY0a9bMeP31121fvk7O+zUM+5ZvXl6eER8fb6Snp5e7R2+Wcb2v0SkuLta6des0ZMgQ1z4/Pz8NGTJEK1euNDEy3/rpp58UHR2tdu3a6YYbblBWVpbZIdWKzMxM7d+/v1x5h4WFqV+/frYub0lasmSJIiIi1LFjR9111106fPiw2SF5TU5OjiQpPDxckrRu3TqdPHmyXDl36tRJbdq0sUU5//p+nd5++201b95c3bp1U0pKigoLC80Iz+tKSko0Z84cFRQUqH///rYv31/fr5Mdyzc5OVnDhw8vV5aSd/+G692inr926NAhlZSUqGXLluX2t2zZUlu2bDEpKt/q16+f0tLS1LFjR2VnZ2vq1Km69NJLtXnzZoWGhpodnk/t379fkiosb+drdnTllVfq97//veLi4rRjxw49/PDDSkpK0sqVK9WgQQOzw6uR0tJSTZo0SQMGDFC3bt0knS7ngIAANW3atNyxdijniu5Xkq6//nq1bdtW0dHR2rhxox566CFt3bpVH3zwgYnR1symTZvUv39/nThxQiEhIVqwYIG6dOmijIwMW5ZvZfcr2bN858yZo/Xr12vNmjXnvObNv+F6n+jUR0lJSa7tHj16qF+/fmrbtq3ee+893XrrrSZGBl/54x//6Nru3r27evTooQsvvFBLlizR4MGDTYys5pKTk7V582Zb9TM7n8ru909/+pNru3v37oqKitLgwYO1Y8cOXXjhhbUdpld07NhRGRkZysnJ0fvvv69x48Zp6dKlZoflM5Xdb5cuXWxXvnv27NG9996r9PR0BQUF+fS96n3TVfPmzdWgQYNzenIfOHBAkZGRJkVVu5o2baoOHTpo+/btZofic84yrc/lLUnt2rVT8+bNLV/mEyZM0CeffKLFixerdevWrv2RkZEqLi7WsWPHyh1v9XKu7H4r0q9fP0mydBkHBASoffv26tWrl1JTU5WQkKCZM2fatnwru9+KWL18161bp4MHD6pnz57y9/eXv7+/li5dqueff17+/v5q2bKl18q43ic6AQEB6tWrlxYtWuTaV1paqkWLFpVrG7Wz/Px87dixQ1FRUWaH4nNxcXGKjIwsV965ubn69ttv6015S9LevXt1+PBhy5a5YRiaMGGCFixYoK+//lpxcXHlXu/Vq5caNmxYrpy3bt2qrKwsS5ZzVfdbkYyMDEmybBlXpLS0VEVFRbYr38o477ciVi/fwYMHa9OmTcrIyHA9evfurRtuuMG17bUy9l7faeuaM2eOERgYaKSlpRk//PCD8ac//clo2rSpsX//frND84n777/fWLJkiZGZmWmsWLHCGDJkiNG8eXPj4MGDZofmFXl5ecaGDRuMDRs2GJKMZ5991tiwYYOxe/duwzAM48knnzSaNm1qfPjhh8bGjRuNa665xoiLizOOHz9ucuTVd757zsvLMx544AFj5cqVRmZmpvHVV18ZPXv2NOLj440TJ06YHXq13HXXXUZYWJixZMkSIzs72/UoLCx0HXPnnXcabdq0Mb7++mtj7dq1Rv/+/Y3+/fubGHX1VXW/27dvNx5//HFj7dq1RmZmpvHhhx8a7dq1My677DKTI6++v/zlL8bSpUuNzMxMY+PGjcZf/vIXw+FwGF9++aVhGPYqX8M4//3asXwr8uuRZd4qYxKdM1544QWjTZs2RkBAgNG3b19j1apVZofkM6NHjzaioqKMgIAAo1WrVsbo0aON7du3mx2W1yxevNiQdM5j3LhxhmGcHmL+t7/9zWjZsqURGBhoDB482Ni6dau5QdfQ+e65sLDQGDp0qNGiRQujYcOGRtu2bY3bb7/d0ol8RfcqyXjzzTddxxw/fty4++67jWbNmhmNGjUyfve73xnZ2dnmBV0DVd1vVlaWcdlllxnh4eFGYGCg0b59e+PBBx80cnJyzA28Bm655Rajbdu2RkBAgNGiRQtj8ODBriTHMOxVvoZx/vu1Y/lW5NeJjrfK2GEYhlHNmicAAIA6rd730QEAAPZFogMAAGyLRAcAANgWiQ4AALAtEh0AAGBbJDoAAMC2SHQAAIBtkegAAADbItEBUOvGjx+vkSNHmvb+N910k6ZNm+aVaxUXFys2NlZr1671yvUAeBczIwPwKofDcd7XH330Ud13330yDENNmzatnaDK+O677zRo0CDt3r1bISEhXrnmiy++qAULFpRbgBBA3UCiA8Cr9u/f79qeO3eupkyZoq1bt7r2hYSEeC3BqI7bbrtN/v7+euWVV7x2zaNHjyoyMlLr169X165dvXZdADVH0xUAr4qMjHQ9wsLC5HA4yu0LCQk5p+lq4MCBmjhxoiZNmqRmzZqpZcuWeu2111RQUKCbb75ZoaGhat++vf773/+We6/NmzcrKSlJISEhatmypW666SYdOnSo0thKSkr0/vvva8SIEeX2x8bGatq0abrlllsUGhqqNm3a6NVXX3W9XlxcrAkTJigqKkpBQUFq27atUlNTXa83a9ZMAwYM0Jw5c2r4rwfA20h0ANQJb731lpo3b67Vq1dr4sSJuuuuu3Tdddfp4osv1vr16zV06FDddNNNKiwslCQdO3ZMgwYNUmJiotauXavPP/9cBw4c0KhRoyp9j40bNyonJ0e9e/c+57UZM2aod+/e2rBhg+6++27dddddrpqo559/Xh999JHee+89bd26VW+//bZiY2PLnd+3b19988033vsHAeAVJDoA6oSEhAQ98sgjio+PV0pKioKCgtS8eXPdfvvtio+P15QpU3T48GFt3LhR0ul+MYmJiZo2bZo6deqkxMREvfHGG1q8eLG2bdtW4Xvs3r1bDRo0UERExDmvXXXVVbr77rvVvn17PfTQQ2revLkWL14sScrKylJ8fLwuueQStW3bVpdcconGjBlT7vzo6Gjt3r3by/8qAGqKRAdAndCjRw/XdoMGDXTBBReoe/furn0tW7aUJB08eFDS6U7FixcvdvX5CQkJUadOnSRJO3bsqPA9jh8/rsDAwAo7TJd9f2dzm/O9xo8fr4yMDHXs2FH33HOPvvzyy3PODw4OdtU2Aag7/M0OAAAkqWHDhuWeOxyOcvucyUlpaakkKT8/XyNGjNBTTz11zrWioqIqfI/mzZursLBQxcXFCggIqPL9ne/Vs2dPZWZm6r///a+++uorjRo1SkOGDNH777/vOv7IkSNq0aKFu7cLoJaQ6ACwpJ49e2r+/PmKjY2Vv797H2UXXXSRJOmHH35wbburSZMmGj16tEaPHq0//OEPuvLKK3XkyBGFh4dLOt0xOjEx0aNrAvA9mq4AWFJycrKOHDmiMWPGaM2aNdqxY4e++OIL3XzzzSopKanwnBYtWqhnz55avny5R+/17LPP6t1339WWLVu0bds2zZs3T5GRkeXmAfrmm280dOjQmtwSAB8g0QFgSdHR0VqxYoVKSko0dOhQde/eXZMmTVLTpk3l51f5R9ttt92mt99+26P3Cg0N1fTp09W7d2/16dNHu3bt0meffeZ6n5UrVyonJ0d/+MMfanRPALyPCQMB1CvHjx9Xx44dNXfuXPXv398r1xw9erQSEhL08MMPe+V6ALyHGh0A9UpwcLBmz5593okFPVFcXKzu3bvrvvvu88r1AHgXNToAAMC2qNEBAAC2RaIDAABsi0QHAADYFokOAACwLRIdAABgWyQ6AADAtkh0AACAbZHoAAAA2yLRAQAAtvX/ATmezirBcyO9AAAAAElFTkSuQmCC",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from qupulse.pulses import ForLoopPT\n",
+ "\n",
+ "for_loop_pt = ForLoopPT(first_point_pt, 't', ('t_start', 't_end', 2))\n",
+ "\n",
+ "print(\"for loop parameters: {}\".format(for_loop_pt.parameter_names))\n",
+ "print(\"for loop measurements: {}\".format(for_loop_pt.measurement_names))\n",
+ "\n",
+ "# plot it\n",
+ "parameters['t_start'] = 4\n",
+ "parameters['t_end'] = 13\n",
+ "_ = plot(for_loop_pt, parameters, sample_rate=100)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The second argument to `ForLoopPT`'s constructor is the name of the loop parameter. This has to be a parameter that is defined by the subtemplate. The third argument defined the range of the loop. The syntax of the range is similar to that of the `range()` command in Python, i.e., a tuple `(start_value, end_value, step)`. As seen above, inserting parameter values or even expressions is okay. As in `range()`, the `end_value` is exclusive.\n",
+ "\n",
+ "As for `SequencePT` and `RepetitionPT`, `ForLoopPT` exposes all parameters defined by the subtemplate except for the loop parameter, `t` in the above example. If expressions are used in the range definition and they make use of additional parameters, these are also exposed by `ForLoopPT`. \n",
+ "\n",
+ "`ForLoopPT` also exposes measurements defined by subtemplates.\n",
+ "\n",
+ "## AtomicMultiChannelPulseTemplate: Run Pulses in Parallel on Different Channels\n",
+ "\n",
+ "So far we have only looked at pulses that affect the time-domain aspect of combining pulses. Another way to combine pulses is to parallelise them by executing them on different channels at the same time. This is of course already supported by simply creating atomic pulse templates (`TablePT`, `PointPT`, `FunctionPT`) on multiple channels. However, sometimes it is necessary to put already existing pulses in parallel. Instead of having to define a new atomic pulse template for this, we can make use of the `AtomicMuliChannelPulseTemplate` class. To learn more about how this works, see [Multi-Channel Pulses](00MultiChannelTemplates.ipynb).\n",
+ "\n",
+ "## Combining Combined Pulses\n",
+ "\n",
+ "Our examples above have build combined higher-level pulses (`SequencePT`, `RepetitionPT`, `ForLoopPT`) on atomic subtemplates only. However, this is not a requirement. We can use `SequencePT`, `RepetitionPT` and `ForLoopPT` using any `PulseTemplate` objects as subtemplates allowing us to build arbitrarily complex pulses out of only a handful of primitives."
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/doc/source/examples/00ConstantPulseTemplate.ipynb b/doc/source/examples/00ConstantPulseTemplate.ipynb
new file mode 100644
index 000000000..0dafbe501
--- /dev/null
+++ b/doc/source/examples/00ConstantPulseTemplate.ipynb
@@ -0,0 +1,72 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# The ConstantPulseTemplate: Efficient constant voltage description.\n",
+ "\n",
+ "The `ConstantPulseTemplate`(or short `ConstantPT`) can be used to define pulse templates with all channels a constant value. The template is easy to define and allows backends to optimize the waveforms on an AWG."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "{'B', 'A'}\n",
+ "{'A': ExpressionScalar('10.0000000000000'), 'B': ExpressionScalar('1.0*b')}\n"
+ ]
+ }
+ ],
+ "source": [
+ "from qupulse.pulses import ConstantPT\n",
+ "\n",
+ "constant_template = ConstantPT(10, {'A': 1., 'B': 'b * 0.1'})\n",
+ "\n",
+ "print(constant_template.defined_channels)\n",
+ "print(constant_template.integral)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The pulse template has two channels."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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8vA43Gzbtq3Eez8/P9WrV08zZsy47pVUAAAAFc3jcLNu3bqKqAMAAMAruIkfAAAwitfCzZgxYzgtBQAAfM5rD848evSojhw54q3hAAC4LofDoYsXL/q6DHhJYGCgVx7C7bVw8/rrr3trKAAArsmyLOXm5urMmTO+LgVe5Ofnp8TERAUGBt7QOF4LNwAAVJYrwSYqKkohISGy2Wy+Lgk3yOl06tixYzp+/LgaNGhwQ79pucJNYWGhPv30Ux0+fFjFxcUlPhsxYkS5iwEA4HocDocr2NStW9fX5cCL6tWrp2PHjunSpUuqUaNGuccp131u7rzzTp0/f16FhYWqU6eOTp06pZCQEEVFRRFuAAAV6socm5CQEB9XAm+7cjrK4XDcULjxeNbOyJEjlZGRoR9//FHBwcH68ssv9f3336tdu3Z6+eWXy10IAACe4FSUebz1m3ocbnJycvTHP/5Rfn5+8vf3V1FRkeLj4zVt2jSNGTPGK0UBAACUl8fhpkaNGq7LtKKionT48GFJUkREBJeCAwBQTocOHZLNZlNOTo6vS3HLbbfdpszMTF+XUSaPw01KSoo2b94sSeratavGjx+vN998U5mZmWrZsqXXCwQAANXPkiVLZLPZXK/Q0FC1a9dO77zzToWv2+NwM3nyZMXGxkqSJk2apNq1a+uxxx7TyZMnNX/+fK8XCAAAqqfw8HAdP35cx48f17Zt25Senq4+ffpoz549Fbpej8NNamqqunXrJunyaanVq1eroKBAW7ZsUXJystcLBADAFE6nU9OmTVPjxo1lt9vVoEEDTZo0qUSfAwcOqFu3bgoJCVFycrI2btzo+uyHH35Qv379VL9+fYWEhKhVq1Z66623Six/2223acSIEXr66adVp04dxcTE6LnnnivRx2azaeHChbrnnnsUEhKipKQkrVq1qkSfHTt2qFevXgoNDVV0dLQGDBigU6dOefR9bTabYmJiFBMTo6SkJL344ovy8/PT9u3bPRrHUzw4EwBQ7VmWpfPFl3zysizL7TpHjx6tl156SePGjdPOnTu1dOlSRUdHl+jz7LPP6qmnnlJOTo6aNGmifv366dKlS5Kkn376Se3atdP777+vHTt26NFHH9WAAQO0adOmEmO8/vrrqlmzpr766itNmzZNzz//vNauXVuiz8SJE9WnTx9t375dd955px544AGdPn1aknTmzBl1795dKSkp+vrrr7V69Wrl5eWpT58+5fl5JF2+vPvK0wzatm1b7nHc4dZ9bu644w4999xz+tWvfnXNfmfPntVrr72m0NBQDRs2zCsFAgBwPRcuOtR8/BqfrHvn8+kKCbz+7vTs2bN69dVXNWfOHA0aNEiSdPPNN6tLly4l+j311FO66667JF0OIC1atNC+ffvUrFkz1a9fX0899ZSr7xNPPKE1a9ZoxYoVat++vau9devWmjBhgiQpKSlJc+bMUXZ2tm6//XZXnwcffFD9+vWTdHnKyezZs7Vp0ybdcccdmjNnjlJSUjR58mRX/8WLFys+Pl579+5VkyZN3No2+fn5Cg0NlSRduHBBNWrU0Pz583XzzTe7tXx5uRVu7r//ft17772KiIhQRkaGUlNTFRcXp6CgIP3444/auXOn1q9frw8++EB33XWXpk+fXqFFAwBQ3ezatUtFRUXq0aPHNfu1bt3a9feVOa4nTpxQs2bN5HA4NHnyZK1YsUJHjx5VcXGxioqKSt3Q8OdjXBnnxIkTV+1Ts2ZNhYeHu/p88803WrdunSuY/Nz+/fvdDjdhYWHaunWrJOn8+fP6+OOPNXToUNWtW1cZGRlujVEeboWbhx9+WP3799df/vIXLV++XPPnz1d+fr6ky+fTmjdvrvT0dG3evFm33HJLhRULAEBZgmv4a+fz6T5bt1v9goPd6vfzO/Neuamd0+mUJE2fPl2vvvqqZs2apVatWqlmzZrKzMws9Sikf727r81mc43hTp9z584pIyNDU6dOLVXflcDlDj8/PzVu3Nj1vnXr1vroo480depU34cbSbLb7erfv7/69+8v6fKhpgsXLqhu3bo3dItkAABulM1mc+vUkC8lJSUpODhY2dnZGjJkSLnG2LBhg37729+69sVOp1N79+5V8+bNvVmq2rZtq5UrVyohIUEBAd7drv7+/rpw4YJXx/xX5Z5QHBERoZiYGIINAABuCAoK0qhRo/T000/rjTfe0P79+/Xll19q0aJFbo+RlJSktWvX6osvvtCuXbv0hz/8QXl5eV6vddiwYTp9+rT69eunzZs3a//+/VqzZo0GDx4sh8Ph9jiWZSk3N1e5ubk6ePCg5s+frzVr1ui3v/2t12v+uaodcwEAMMi4ceMUEBCg8ePH69ixY4qNjdXQoUPdXn7s2LE6cOCA0tPTFRISokcffVS9e/d2TRXxlri4OG3YsEGjRo1Sz549VVRUpIYNG+qOO+5wPaXAHQUFBa7TWHa7XQ0bNtTzzz+vUaNGebXef2WzPLmGzQAFBQWKiIhQfn6+wsPDfV0OAMBDP/30kw4ePKjExEQFBQX5uhx40bV+W0/239znBgAAGIVwAwAAjFKucHPmzBktXLhQo0ePdt3NcOvWrTp69KhXiwMAAPCUxxOKt2/frrS0NEVEROjQoUN65JFHVKdOHb3zzjs6fPiw3njjjYqoEwAAwC0eH7nJysrSgw8+qO+++67EZJ8777xTn332mVeLAwAA8JTH4Wbz5s36wx/+UKq9fv36ys3N9biAuXPnKiEhQUFBQerQoUOph3/9qzNnzmjYsGGKjY2V3W5XkyZN9MEHH3i8XgAAYCaPT0vZ7XYVFBSUat+7d6/q1avn0VjLly9XVlaW5s2bpw4dOmjWrFlKT0/Xnj17FBUVVap/cXGxbr/9dkVFRentt99W/fr19f3336tWrVqefg0AAGAoj4/c3H333Xr++ed18eJFSZdveX348GGNGjVK9957r0djzZw5U4888ogGDx6s5s2ba968eQoJCdHixYvL7L948WKdPn1a7733njp37qyEhAR17dpVycnJnn4NAABgKI/DzYwZM3Tu3DlFRUXpwoUL6tq1qxo3bqywsDBNmjTJ7XGKi4u1ZcsWpaWl/bMYPz+lpaVp48aNZS6zatUqdezYUcOGDVN0dLRatmypyZMnX/NW0EVFRSooKCjxAgAA5vL4tFRERITWrl2r9evXa/v27Tp37pzatm1bIqS449SpU3I4HIqOji7RHh0drd27d5e5zIEDB/T3v/9dDzzwgD744APt27dPjz/+uC5evKgJEyaUucyUKVM0ceJEj2oDAKCyHTp0SImJidq2bZvatGnj63Ku67bbblObNm00a9YsX5dSSrmfLdWlSxd16dLFm7Vcl9PpVFRUlObPny9/f3+1a9dOR48e1fTp068abkaPHq2srCzX+4KCAsXHx1dWyQAA/KJduHBB9evXl5+fn44ePSq73V7h6/Q43MyePbvMdpvNpqCgIDVu3Fj/7//9P/n7+19znMjISPn7+5d6mmleXp5iYmLKXCY2NlY1atQoMfYtt9yi3NxcFRcXKzAwsNQydru9UjYkAAAobeXKlWrRooUsy9J7772nvn37Vvg6PZ5z88orr2jMmDHKzMzUxIkTNXHiRGVmZmr06NEaN26cevTooaZNm+rIkSPXHCcwMFDt2rVTdna2q83pdCo7O1sdO3Ysc5nOnTtr3759cjqdrra9e/cqNja2zGADAEBV4nQ6NW3aNDVu3Fh2u10NGjQoNV/1wIED6tatm0JCQpScnFxiHuoPP/ygfv36qX79+goJCVGrVq301ltvlVj+tttu04gRI/T000+rTp06iomJ0XPPPVeij81m08KFC3XPPfcoJCRESUlJWrVqVYk+O3bsUK9evRQaGqro6GgNGDBAp06d8vg7L1q0SP3791f//v21aNEij5cvF8tDS5cutW677TZr3759rrbvvvvO6t69u7Vs2TLryJEjVufOna177733umMtW7bMstvt1pIlS6ydO3dajz76qFWrVi0rNzfXsizLGjBggPXMM8+4+h8+fNgKCwuzhg8fbu3Zs8f629/+ZkVFRVkvvvii2/Xn5+dbkqz8/HwPvjUAoKq4cOGCtXPnTuvChQv/bHQ6LavonG9eTqfbtT/99NNW7dq1rSVLllj79u2zPv/8c2vBggWWZVnWwYMHLUlWs2bNrL/97W/Wnj17rPvuu89q2LChdfHiRcuyLOsf//iHNX36dGvbtm3W/v37rdmzZ1v+/v7WV1995VpH165drfDwcOu5556z9u7da73++uuWzWazPvroI1cfSdZNN91kLV261Pruu++sESNGWKGhodYPP/xgWZZl/fjjj1a9evWs0aNHW7t27bK2bt1q3X777Va3bt1KrOfJJ5+85vfdt2+fZbfbrdOnT1s//PCDFRQUZB06dMiz3/b/eLL/tv3fl3TbzTffrJUrV5aa7LRt2zbde++9OnDggL744gvde++9On78+HXHmzNnjqZPn67c3Fy1adNGs2fPVocOHSRdTp8JCQlasmSJq//GjRs1cuRI5eTkqH79+nr44Yc1atSo654Gu8KTR6YDAKqen376SQcPHlRiYuI/75RfXChNjvNNQWOOSYE1r9vt7NmzqlevnubMmaMhQ4aU+vzKhOKFCxfq4YcfliTt3LlTLVq00K5du9SsWbMyx/3Nb36jZs2a6eWXX5Z0ed/pcDj0+eefu/q0b99e3bt310svvSTp8pGbsWPH6oUXXpAkFRYWKjQ0VB9++KHuuOMOvfjii/r888+1Zs0a1xj/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8EHl5OSoadOmGjFihD766KNSywcHB7uOKgHwrQBfFwAAklSjRo0S7202W4m2K4HE6XRKks6dO6eMjAxNnTq11FixsbFlriMyMlLnz59XcXGxAgMDr7v+K+tq27atDh48qA8//FAff/yx+vTpo7S0NL399tuu/qdPn1a9evXc/boAKhDhBkC11LZtW61cuVIJCQkKCHDvn7I2bdpIknbu3On6213h4eHq27ev+vbtq/vuu0933HGHTp8+rTp16ki6PLk5JSXFozEBVAxOSwGoloYNG6bTp0+rX79+2rx5s/bv3681a9Zo8ODBcjgcZS5Tr149tW3bVuvXr/doXTNnztRbb72l3bt3a+/evfrLX/6imJiYEvfp+fzzz9WzZ88b+UoAvIRwA6BaiouL04YNG+RwONSzZ0+1atVKmZmZqlWrlvz8rv5P25AhQ/Tmm296tK6wsDBNmzZNqampuvXWW3Xo0CF98MEHrvVs3LhR+fn5uu+++27oOwHwDm7iB+AX5cKFC2ratKmWL1+ujh07emXMvn37Kjk5WWPGjPHKeABuDEduAPyiBAcH64033rjmzf48UVxcrFatWmnkyJFeGQ/AjePIDQAAMApHbgAAgFEINwAAwCiEGwAAYBTCDQAAMArhBgAAGIVwAwAAjEK4AQAARiHcAAAAoxBuAACAUf4/vUa+J2Znxp4AAAAASUVORK5CYII=",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from qupulse.pulses.plotting import plot\n",
+ "\n",
+ "_ = plot(constant_template, parameters={'b': 2.2}, sample_rate=100)"
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/doc/source/examples/00FunctionPulse.ipynb b/doc/source/examples/00FunctionPulse.ipynb
new file mode 100644
index 000000000..1bb33ff91
--- /dev/null
+++ b/doc/source/examples/00FunctionPulse.ipynb
@@ -0,0 +1,86 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Modelling Pulses Using Functions And Expressions\n",
+ "\n",
+ "Assume we want to model a pulse that represents a damped sine function. While we could, in theory, do this using `TablePulseTemplate`s by piecewise linear approximation (cf. [Modelling a Simple TablePulseTemplate](00SimpleTablePulse.ipynb)), this would be a tedious endeavor. A much simpler approach presents itself in the form of the `FunctionPulseTemplate` class of qupulse. Like the `TablePulseTemplate`, a `FunctionPulseTemplate` represents an atomic pulse which will be converted into a waveform for execution. The difference between both is that `FunctionPulseTemplate` accepts a mathematical expression which is parsed and evaluated using `sympy` to sample the waveform instead of the linear interpolation between specified supporting points as it is done in `TablePulseTemplate`.\n",
+ "\n",
+ "To define the sine function pulse template, we can thus do the following:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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0tJSlZeXKzAw8LKvX/VavXv31qFDh/Tvf/9b7733nu644w4lJyfr73//u2X/U6dOqVWrVra+XQBugiAEwCP17t1ba9asUVxcnJo0se2rrGfPnpKkffv2Wf62VXh4uFJTU5WamqrbbrtNN998s06dOqXIyEhJP3Tc7tWrl13HBOB6XBoD4JHuu+8+nTp1SiNGjNC2bduUl5endevWKT09XRUVFbU+p1WrVurdu7c2bdpk12vNnz9ff/vb3/T555/r4MGDev311xUTE2M1D9JHH32km266qTFvCYALEIQAeKS2bdtq8+bNqqio0E033aTu3btr0qRJatGihfz96/5qGzt2rF599VW7XissLExz5sxRYmKi+vbtq8OHD+vdd9+1vE5ubq6Kiop02223Neo9AXA+JlQE4FPOnz+vTp06afXq1erfv78px0xNTVWPHj30yCOPmHI8AM5DixAAn9KsWTO9/PLL9U68aI/y8nJ1795dkydPNuV4AJyLFiEAAOCzaBECAAA+iyAEAAB8FkEIAAD4LIIQAADwWQQhAADgswhCAADAZxGEAACAzyIIAQAAn0UQAgAAPuv/AHgd3ndcu7PEAAAAAElFTkSuQmCC",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from qupulse.pulses import FunctionPT\n",
+ "from qupulse.plotting import plot\n",
+ "\n",
+ "template = FunctionPT('exp(-t/2)*sin(2*t / pi)', '2')\n",
+ "\n",
+ "_ = plot(template, sample_rate=100)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The first argument to `FunctionPulseTemplate`'s constructor is the string representation of the formula that the pulse represents. The second argument is used to compute the length of the pulse. In this case, this is simply a constant expression. Refer to [sympy's documentation](http://docs.sympy.org/latest/index.html) to read about the usable operators and functions in the expressions.\n",
+ "\n",
+ "The `t` is reserved as the free variable of the time domain in the first argument and must be present. Other variables can be used at will and corresponding values have to be passed in as a parameter when instantiating a pulse for execution from the created `FunctionPulseTemplate` object:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "C:\\Users\\Simon\\Documents\\git\\qupulse\\qupulse\\plotting.py:186: UserWarning: Sample count 6288/5 is not an integer. Will be rounded (this changes the sample rate).\n",
+ " times, voltages, measurements = render(program,\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAksAAAGwCAYAAAC5ACFFAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAACRS0lEQVR4nO3dd3xT9foH8E/SvQfdrFJWQcqGslSEXuYVuS70hyKIoCh6Ab0iXkFFBcGN1yuK4roO3FsEKkORDWXIXmV1UEo3XUl+fyTn5KTNOElOVvt5v159cZqcnHMS2ubJ9/t8n0el0+l0ICIiIiKz1J6+ACIiIiJvxmCJiIiIyAoGS0RERERWMFgiIiIisoLBEhEREZEVDJaIiIiIrGCwRERERGSFv6cvoCnQarW4cOECIiIioFKpPH05REREJINOp0N5eTlSUlKgVlseP2KwpIALFy6gdevWnr4MIiIicsDZs2fRqlUri/czWFJAREQEAP2LHRkZ6eGrISIiIjnKysrQunVr8X3cEgZLChCm3iIjIxksERER+RhbKTRM8CYiIiKygsESERERkRUMloiIiIisYM4SERG5lUajQV1dnacvg5qBgIAA+Pn5OX0cBktEROQWOp0O+fn5KCkp8fSlUDMSHR2NpKQkp+ogMlgiIiK3EAKlhIQEhIaGsogvuZROp0NVVRUKCwsBAMnJyQ4fi8ESERG5nEajEQOlFi1aePpyqJkICQkBABQWFiIhIcHhKTkmeBMRkcsJOUqhoaEevhJqboSfOWfy5BgsERGR23DqjdxNiZ85BktEREREVjBYIiIiIrKCwRIREZGDTp8+DZVKhZycHE9fiixDhw7FrFmz7HrMU089hZ49e9r1mMOHD2PAgAEIDg62+7HWOHL9SmCwRERERIp68sknERYWhiNHjiA7O9tl50lNTcWrr77qsuMLWDqAiIiIFHXixAmMHTsWbdu29fSlKIIjS0RE5BE6nQ5VtfUe+dLpdLKvU6vVYunSpejQoQOCgoLQpk0bPPfccyb7nDx5Etdddx1CQ0PRo0cPbNmyRbzv0qVLuP3229GyZUuEhoYiIyMDn376qcnjhw4dioceegiPPvooYmNjkZSUhKeeespkH5VKhXfeeQf/+Mc/EBoaio4dO+L777832efAgQMYPXo0wsPDkZiYiDvvvBNFRUWynysAPP/880hMTERERASmTp2K6urqRvu888476NKlC4KDg5Geno7//ve/Jte5a9cuLFy4ECqVSnwec+fORadOnRAaGoq0tDTMnz/fZDn/5MmTMX78eJPzzJo1C0OHDjV7nUOHDkVubi5mz54NlUrl0pWWHFkiIiKPuFKnQdcFv3rk3AcXjkRooLy3wHnz5mHFihV45ZVXMGTIEOTl5eHw4cMm+/z73//Giy++iI4dO+Lf//43br/9dhw/fhz+/v6orq5Gnz59MHfuXERGRuKnn37CnXfeifbt26N///7iMT744APMmTMH27Ztw5YtWzB58mQMHjwYf/vb38R9nn76aSxduhQvvPACXn/9dUycOBG5ubmIjY1FSUkJhg0bhnvuuQevvPIKrly5grlz5+LWW2/Fb7/9Juu5fv7553jqqafwxhtvYMiQIfjoo4+wbNkypKWlift8/PHHWLBgAf7zn/+gV69e2LNnD6ZNm4awsDDcddddyMvLQ1ZWFkaNGoVHHnkE4eHhAICIiAi8//77SElJwf79+zFt2jRERETg0UcflXVtDX399dfo0aMHpk+fjmnTpjl0DLl8amRp06ZNuP7665GSkgKVSoVvv/3W5mM2bNiA3r17IygoCB06dMD777/faJ833ngDqampCA4ORmZmJrZv3678xRMRkc8pLy/Ha6+9hqVLl+Kuu+5C+/btMWTIENxzzz0m+z3yyCMYO3YsOnXqhKeffhq5ubk4fvw4AKBly5Z45JFH0LNnT6SlpeHBBx/EqFGj8Pnnn5sco3v37njyySfRsWNHTJo0CX379m2U7zN58mTcfvvt6NChAxYtWoSKigrxPUsIXhYtWoT09HT06tULK1euxPr163H06FFZz/fVV1/F1KlTMXXqVHTu3BnPPvssunbtarLPk08+iZdeegk33ngj2rVrhxtvvBGzZ8/GW2+9BQBISkqCv78/wsPDkZSUJAZLTzzxBAYNGoTU1FRcf/31eOSRRxq9BvaIjY2Fn58fIiIikJSUhKSkJIePZYtPjSxVVlaiR48euPvuu3HjjTfa3P/UqVMYO3Ys7rvvPnz88cfIzs7GPffcg+TkZIwcORIAsGrVKsyZMwfLly9HZmYmXn31VYwcORJHjhxBQkKCq58SEVGzFRLgh4MLR3rs3HIcOnQINTU1GD58uNX9unfvLm4LPcgKCwuRnp4OjUaDRYsW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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "param_template = FunctionPT('exp(-t/tau)*sin(phi*t)', 'duration')\n",
+ "\n",
+ "_ = plot(param_template, {'tau': 4, 'phi': 8, 'duration': 4*3.1415}, sample_rate=100)"
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 1
+}
diff --git a/doc/source/examples/00MappingTemplate.ipynb b/doc/source/examples/00MappingTemplate.ipynb
new file mode 100644
index 000000000..b9fb1f9d1
--- /dev/null
+++ b/doc/source/examples/00MappingTemplate.ipynb
@@ -0,0 +1,284 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Mapping with the MappingPulseTemplate\n",
+ "\n",
+ "We will now have a look on how to remap parameters, channel ids and measurements. The definition of measurements is illustrated in [Definition of Measurements](01Measurements.ipynb). The `MappingPulseTemplate` class allows us to take any already existing `PulseTemplate` and specify a mapping for its parameters, channel ids and measurements.\n",
+ "\n",
+ "This can be useful for simply renaming things, e.g., to avoid name collisions of parameters or change the name of a channel a pulse should be executed on, but can also be employed to derive the value of certain parameters from other parameters.\n",
+ "\n",
+ "## Mapping Parameters"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "2*pi/omega\n",
+ "{'omega', 'a'}\n"
+ ]
+ }
+ ],
+ "source": [
+ "from qupulse.pulses import MappingPT, FunctionPT, AtomicMultiChannelPT\n",
+ "\n",
+ "sine = FunctionPT('a*sin(omega*t)', 't_duration')\n",
+ "\n",
+ "my_parameter_mapping = dict(t_duration='2*pi/omega', omega='omega', a='a')\n",
+ "\n",
+ "single_period_sine = MappingPT(sine, parameter_mapping=my_parameter_mapping)\n",
+ "\n",
+ "print(single_period_sine.duration)\n",
+ "print(single_period_sine.parameter_names)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Notice that we had to give mappings for all parameters, not only for the ones which changed. If we omit some of the encapsulated pulse tempaltes parameters an `MissingMappingException` is raised. This is done to enforce active thinking.\n",
+ "\n",
+ "You can, however, allow partial parameter mappings by passing `allow_partial_paramter_mappings=True` to the constructor."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "we expect an exception here:\n",
+ "\n",
+ "no exception with allow_partial_parameter_mapping=True\n",
+ "2*pi/omega\n",
+ "{'omega', 'a'}\n"
+ ]
+ }
+ ],
+ "source": [
+ "partial_parameter_mapping = dict(t_duration='2*pi/omega')\n",
+ "print('we expect an exception here:')\n",
+ "try:\n",
+ " single_period_sine = MappingPT(sine, parameter_mapping=partial_parameter_mapping)\n",
+ "except Exception as exception:\n",
+ " print(type(exception).__name__, ':', exception)\n",
+ "print('')\n",
+ "\n",
+ "print('no exception with allow_partial_parameter_mapping=True')\n",
+ "single_period_sine = MappingPT(sine, parameter_mapping=partial_parameter_mapping, allow_partial_parameter_mapping=True)\n",
+ "print(single_period_sine.duration)\n",
+ "print(single_period_sine.parameter_names)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Mapping of Channel Ids and Measurement Names\n",
+ "\n",
+ "Sometimes it is necessary to rename channels or measurements. Here we see a case where we want to play a sine and a cosine in parallel by using the `AtomicMultiChannelPulseTemplate` (for a more in depth explanation of multi-channel pulse template, see [Multi-Channel Pulses](00MultiChannelTemplates.ipynb)). Of course, this doesn't work as both pulses are by default defined on the 'default' channel."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "ChannelMappingException : Channel is defined in subtemplate 1 and subtemplate 2\n"
+ ]
+ }
+ ],
+ "source": [
+ "sine_measurements = [('M', 't_duration/2', 't_duration')]\n",
+ "sine = FunctionPT('a*sin(omega*t)', 't_duration', measurements=sine_measurements)\n",
+ "\n",
+ "cos_measurements = [('M', 0, 't_duration/2')]\n",
+ "cos = FunctionPT('a*cos(omega*t)', 't_duration', measurements=cos_measurements)\n",
+ "\n",
+ "try:\n",
+ " both = AtomicMultiChannelPT(sine, cos)\n",
+ "except Exception as exception:\n",
+ " print(type(exception).__name__, ':', exception)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The solution is to use the `MappingPT` and rename the channels as we see in the next cell. Additionally, we want to distinguish between the measurements, so we rename them, too. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "remapped_cos channels: {'cos_channel'}\n",
+ "remapped_cos measurements: {'M_cos'}\n",
+ "\n",
+ "remapped_sine channels: {'sin_channel'}\n",
+ "remapped_sine measurements: {'M_sin'}\n",
+ "\n",
+ "{'cos_channel', 'sin_channel'}\n",
+ "{'M_sin', 'M_cos'}\n"
+ ]
+ }
+ ],
+ "source": [
+ "cos_channel_mapping = dict(default='cos_channel')\n",
+ "cos_measurement_mapping = dict(M='M_cos')\n",
+ "remapped_cos = MappingPT(cos, channel_mapping=cos_channel_mapping, measurement_mapping=cos_measurement_mapping)\n",
+ "print('remapped_cos channels:', remapped_cos.defined_channels)\n",
+ "print('remapped_cos measurements:', remapped_cos.measurement_names)\n",
+ "print()\n",
+ "\n",
+ "sine_channel_mapping = dict(default='sin_channel')\n",
+ "sine_measurement_mapping = dict(M='M_sin')\n",
+ "remapped_sine = MappingPT(sine, measurement_mapping=sine_measurement_mapping, channel_mapping=sine_channel_mapping)\n",
+ "print('remapped_sine channels:', remapped_sine.defined_channels)\n",
+ "print('remapped_sine measurements:', remapped_sine.measurement_names)\n",
+ "print()\n",
+ "\n",
+ "both = AtomicMultiChannelPT(remapped_sine, remapped_cos)\n",
+ "print(both.defined_channels)\n",
+ "print(both.measurement_names)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let's also plot it to see if it looks like expected with some dummy values for our parameters:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "C:\\Users\\Simon\\Documents\\git\\qupulse\\qupulse\\plotting.py:186: UserWarning: Sample count 6293/10 is not an integer. Will be rounded (this changes the sample rate).\n",
+ " times, voltages, measurements = render(program,\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from qupulse.pulses.plotting import plot\n",
+ "\n",
+ "parameter_values = dict(omega=1.0, a=1.0, t_duration=2*3.1415)\n",
+ "\n",
+ "_ = plot(both, parameters=parameter_values, sample_rate=100)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Automatically created mapping templates\n",
+ "\n",
+ "Besides the explicit usage of the template it is also used implicitly in some cases. All implicit uses make use of the static member function `MappingPulseTemplate.from_tuple`. This 'constructor' automatically decides which mapping belongs to which entity."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "channels: {'default'}\n",
+ "measurements {'M_sin'}\n",
+ "parameters {'omega', 't_duration', 'a'}\n",
+ "\n",
+ "channels: {'default'}\n",
+ "measurements {'M_sin'}\n",
+ "parameters {'omega', 'a'}\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "auto_mapped = MappingPT.from_tuple((sine, sine_measurement_mapping))\n",
+ "print('channels:', auto_mapped.defined_channels)\n",
+ "print('measurements', auto_mapped.measurement_names)\n",
+ "print('parameters', auto_mapped.parameter_names)\n",
+ "print()\n",
+ "\n",
+ "auto_mapped = MappingPT.from_tuple((sine, sine_measurement_mapping, partial_parameter_mapping))\n",
+ "print('channels:', auto_mapped.defined_channels)\n",
+ "print('measurements', auto_mapped.measurement_names)\n",
+ "print('parameters', auto_mapped.parameter_names)\n",
+ "print()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "In many cases, you do not need to create the MappingPT yourself. Most PulseParameters accept a mapping tuple like the ones used in the last cell. We could create our combined pulse also by using this implicit conversion:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "{'cos_channel', 'sin_channel'}\n",
+ "{'M_sin', 'M_cos'}\n"
+ ]
+ }
+ ],
+ "source": [
+ "both_implicit = AtomicMultiChannelPT((sine, sine_channel_mapping, sine_measurement_mapping), \n",
+ " (cos, cos_measurement_mapping, cos_channel_mapping))\n",
+ "print(both_implicit.defined_channels)\n",
+ "print(both_implicit.measurement_names)"
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/doc/source/examples/00MultiChannelTemplates.ipynb b/doc/source/examples/00MultiChannelTemplates.ipynb
new file mode 100644
index 000000000..0084aea70
--- /dev/null
+++ b/doc/source/examples/00MultiChannelTemplates.ipynb
@@ -0,0 +1,230 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Multi-Channel Pulses\n",
+ "\n",
+ "Usually there is a need to define pulses for multiple control channels simulateously. While this would be possible by simply defining several separate pulse templates (one for each channel), qupulse also allows to define pulse templates directly for multiple channels or combine existing templates in a multi-channel way. This tutorial explores these possibilities.\n",
+ "\n",
+ "## A Multi-Channel Table Pulse\n",
+ "`TablePulseTemplate` allows to model multiple channels in a straighforward way: In its constructor entries are given as time-voltage sequences in a dictionary where each key specifies a channel id (which can be an identifier string or a number). In the first few examples we have mostly ignored this but here we are making use of it.\n",
+ "\n",
+ "The following example constructs a 2-channel table pulse template with shared parameters and plots it."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "The number of channels in table_template is 2.\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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9EKkq66IjFLaJc/ZfeXG8uXLBT8/P0EulwvFixcXAgMDhf3796uXly9fXmjTpk2W+w4MDFS/fvPmjTBs2DChdOnSgqWlpVC/fn3h5MmTWuulXb68adMmje3FxcUJAIQVK1Zo7MPHx0dr3yEhIUL58uU1+lJTU4Vp06YJPj4+6vfi7+8vTJgwQUhISNB4P7m5pFsQBGHSpElCmTJlBBMTE43Lu3fs2CFUr15dsLCwENzd3YVp06YJy5cv17oEPO3Y7d27V6hevbogl8sFLy8vrfee+ZLuNBcuXBDat28vlChRQpDL5UL58uWFTp06CeHh4e+t/d27d8KAAQOEUqVKCTKZTOv3YfHixYK/v79gaWkp2NraCr6+vsKIESOEe/fuadWfGQChX79+Gn1pP78ZM2ao+0JCQgRra2shNjZWaNGihWBlZSU4OzsLoaGhWpf/I9Ml3c+fPxd69uwplCxZUrCxsRGCgoKEa9euZfnzW7JkieDh4SGYmppqHceIiAghKChIsLe3FywsLISKFSsKPXr0EM6ePfveY0hUUDJB0NEsMyIiElWPHj2wefPmLE+hERUFnFNDREREksBQQ0RERJLAUENERESSwDk1REREJAkcqSEiIiJJYKghIiIiSShSN99TKpW4d+8ebG1t83RrdyIiIhKPIAh4+fIlXF1dYWKS/XhMkQo19+7dy/ZBe0RERFS43blzB2XLls12eZEKNba2tgBUB8XOzk7kaoiIiCg3EhMT4ebmpv4cz06RCjVpp5zs7OwYaoiIiIzM+6aOcKIwERERSQJDDREREUkCQw0RERFJAkMNERERSQJDDREREUkCQw0RERFJAkMNERERSQJDDREREUkCQw0RERFJAkMNERERSQJDDREREUkCQw0RERFJAkMNERERSQJDDREREUkCQw0RERFJAkMNERERSQJDDREREUkCQw0RERFJAkMNERERSQJDDREREUkCQw0RERFJAkMNERERSQJDDREREUkCQw0RERFJgtGGmqlTp0Imk2Hw4MFil0JERESFgFGGmsjISCxatAjVq1cXuxQiIiIqJIqJXUBeJSUl4YsvvsCSJUvw008/iV2OUXv8MgUp7xRil0FERsbFzgLFTI3y/4lJ4owu1PTr1w9t2rRBs2bN3htqUlJSkJKSon6dmJio7/KMxqoTtxC64x+xyyAiI1SjrD3+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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from qupulse.pulses import TablePT\n",
+ "from qupulse.plotting import plot\n",
+ "\n",
+ "table_template = TablePT(identifier='2-channel-table-template',\n",
+ " entries={'first_channel' : [(0, 0),\n",
+ " (1, 4),\n",
+ " ('foo', 'bar'),\n",
+ " (10, 0)],\n",
+ " 'second_channel': [(0, 0),\n",
+ " ('foo', 2.7, 'linear'),\n",
+ " (9, 'bar', 'linear')]}\n",
+ " )\n",
+ "\n",
+ "parameters = dict(\n",
+ " foo=7,\n",
+ " bar=-1.3\n",
+ ")\n",
+ "_ = plot(table_template, parameters, sample_rate=100)\n",
+ "print(\"The number of channels in table_template is {}.\".format(table_template.num_channels))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Combining Templates: `AtomicMultiChannelPulseTemplate`\n",
+ "\n",
+ "`AtomicMultiChannelPulseTemplate`(`AtomicMultiChannelPT`) allows to compose a multi-channel template out of atomic (i.e., no control flow) templates of equal duration. It allows to reassign channel indices of the channels of its subtemplates. The constructor is similar to the one of `SequencePulseTemplate` and expects subtemplates (including parameter and channel mappings if required).\n",
+ "\n",
+ "The following example will combine the two-channel table pulse template `table_template` from above and a function pulse template `function_template` to a three-channel template `template`. We reassign indices such that channel 'rectangle' of the new `template` is channel 'first_channel' and 'triangle' is channel 'second_channel' of `table_template`. Furthermore the parameters get remapped. `function_template` doesn't get changed at all."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "The number of channels in function_template is 1.\n",
+ "The number of channels in template is 3.\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from qupulse.pulses import FunctionPT, AtomicMultiChannelPT\n",
+ "\n",
+ "function_template = FunctionPT('-sin(t)**2', '10', identifier='function-template', channel='wavy')\n",
+ "\n",
+ "template = AtomicMultiChannelPT(\n",
+ " function_template,\n",
+ " (table_template, dict(foo='5', bar='2 * hugo'), {'first_channel': 'rectangle ', 'second_channel': 'triangle'}),\n",
+ " identifier='3-channel-combined-template'\n",
+ ")\n",
+ "\n",
+ "_ = plot(template, dict(hugo=-1.3), sample_rate=100)\n",
+ "print(\"The number of channels in function_template is {}.\".format(function_template.num_channels))\n",
+ "print(\"The number of channels in template is {}.\".format(template.num_channels))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The constructor of `AtomicMultiChannelPulseTemplate` expects its subtemplates as positional arguments. Each of positional arguments is required to be either a `AtomicPulseTemplate`, a `MappingPulseTemplate` that wraps an `AtomicPulseTemplate` or a tuple that can be passed to `MappingPulseTemplate.from_tuple`(more examples in [Mapping with the MappingPulseTemplate](00MappingTemplate.ipynb)). The sets of channels on which the subtemplates are defined has to be distinct.\n",
+ "Note that an exception will be raised during the sampling of the waveforms (i.e., during the sequencing process) if the subtemplates have different length."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Instantionation duration check\n",
+ "By default the AtomicMultiChannelPulseTemplate checks whether the durations are equal on construction. It is possible to do this check during instantiation (when create_program is called) by providing the `duration` keyword argument. This can either be an expression or `True`. If it is an expression all subwaveforms have to have a duration equal to it. If it is `True` all waveforms have to have an unspecified equal duration."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "Instantionation duration check with specified value\n",
+ "The duration does not equal the expected duration 4\n"
+ ]
+ }
+ ],
+ "source": [
+ "from qupulse.pulses import FunctionPT, AtomicMultiChannelPT\n",
+ "\n",
+ "template_a = FunctionPT('-sin(t)**2', 'duration_a', channel='a')\n",
+ "template_b = FunctionPT('-cos(t)**2', 'duration_b', channel='b')\n",
+ "\n",
+ "try:\n",
+ " # instantiation duration check\n",
+ " template = AtomicMultiChannelPT(\n",
+ " template_a,\n",
+ " template_b,\n",
+ " identifier='3-channel-combined-template'\n",
+ " )\n",
+ "except ValueError as err:\n",
+ " print('Instantiation duration check:')\n",
+ " print(err)\n",
+ "\n",
+ "# instantiation duration check with specified value\n",
+ "template_specified = AtomicMultiChannelPT(\n",
+ " template_a,\n",
+ " template_b,\n",
+ " duration='my_duration'\n",
+ ")\n",
+ "template_specified.create_program(parameters=dict(duration_a=3, duration_b=3, my_duration=3))\n",
+ "try:\n",
+ " template_specified.create_program(parameters=dict(duration_a=3, duration_b=3, my_duration=4))\n",
+ "except ValueError as err:\n",
+ " print()\n",
+ " print('Instantionation duration check with specified value')\n",
+ " print(err.args[0], err.args[1])\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Multiple Channels in Non-Atomic Templates\n",
+ "\n",
+ "All higher order template, i.e., `SequencePulseTemplate` and `ForLoopPulseTemplate` and `RepetitionPulseTemplate`, also support multiple channels insofar as that they can be composed using multi-channel atomic templates as subtemplates. They require that all these subtemplates define the same channels and raise an exception if that is not the case. The following example constructs a `SequencePulseTempate` `sequence_template` by chaining the above defined two-channel `table_template`. In the second instance of `table_template` in the sequence, we swap the channels by wrapping a `MappingPulseTemplate` around it."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "The number of channels in sequence_template is 2.\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from qupulse.pulses import SequencePT\n",
+ "\n",
+ "sequence_template = SequencePT(\n",
+ " (table_template, dict(foo='1.2 * hugo', bar='hugo ** 2')),\n",
+ " (table_template, dict(foo='1.2 * hugo', bar='hugo ** 2'), {'first_channel': 'second_channel', \n",
+ " 'second_channel': 'first_channel'}),\n",
+ " identifier='2-channel-sequence-template'\n",
+ ")\n",
+ "\n",
+ "plot(sequence_template, dict(hugo=2), sample_rate=100)\n",
+ "print(\"The number of channels in sequence_template is {}.\".format(sequence_template.num_channels))"
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 1
+}
diff --git a/doc/source/examples/00PointPulse.ipynb b/doc/source/examples/00PointPulse.ipynb
new file mode 100644
index 000000000..e99cbc0a9
--- /dev/null
+++ b/doc/source/examples/00PointPulse.ipynb
@@ -0,0 +1,80 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# The PointPulseTemplate\n",
+ "\n",
+ "The `PointPulseTemplate`(or short `PointPT`) can be understood as a specialization of the `TablePulseTemplate`. It restricts the channels to all having the same time points in their entries and the same expression for their voltages.\n",
+ "\n",
+ "Let us first have a look at an simple example: "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "{'A', 'B'}\n"
+ ]
+ }
+ ],
+ "source": [
+ "from qupulse.pulses import PointPT\n",
+ "\n",
+ "point_template = PointPT([(0, 'v_0'),\n",
+ " (1, 'v_1', 'linear'),\n",
+ " ('t', 'v_0+v_1', 'jump')],\n",
+ " channel_names=('A', 'B'))\n",
+ "\n",
+ "print(point_template.defined_channels)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "As you can see the pulse template has two channels although we only provided one expression for the voltage per time point. The value of this expression can either be scalar as we will see now for `v_0` or be a `numpy` array of the same length as the number of channels is like `v_1`. A value of the wrong length will result in an exception."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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qMuODQAAvFtdTxcQEBCguLg4SVKXLl20ZcsWzZ07V/PmzbusbXR0tI4fP2637fjx44qOjq7w+IGBgQoMDHRt0QAAwCd4vEeorJKSElmt1nJfS0pKUnp6ut22VatWVTimCAAAoDIe7RFKTU1V37591axZM507d07vvvuu1qxZo5UrV0qShg4dqqZNmyotLU2S9Nhjj6lbt26aPXu2+vfvr/fee09bt27V/PnzPXkaAADAS3k0CJ04cUJDhw5Vdna2wsPD1bFjR61cuVK33367JCkrK0t+fr92WiUnJ+vdd9/V5MmTNWnSJMXHx2v58uW69tprPXUKAADAi9W6+wjVNO4jBJ/HfYQA+CCfv48QAACAuxGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAadV1dger1arNmzfryJEjKiwsVOPGjZWYmKiWLVvWRH0AAAA1xuEgtGHDBs2dO1f//Oc/deHCBYWHhys4OFinT5+W1WpVq1at9NBDD2n06NGqX79+TdYMAADgEg59NXbHHXdo8ODBatGihb788kudO3dOp06d0g8//KDCwkL95z//0eTJk5Wenq42bdpo1apVNV03AABAtTnUI9S/f38tXbpU/v7+5b7eqlUrtWrVSsOGDdPevXuVnZ3t0iIBAABqgkNBaNSoUQ4fsF27dmrXrl2VCwIAAHAXZo0BAADTclkQGjZsmHr06OGqwwEAANQ4p6fPV6Rp06by86ODCQAAeA+XBaHp06e76lAAAABuQRcOAAAwLad7hO6///5KX1+0aJHDx0pLS9NHH32k7777TsHBwUpOTtaMGTOUkJBQ4T6LFy/WiBEj7LYFBgbq559/dvh9AQAApCoEoTNnztg9v3Dhgnbv3q2zZ886PVh67dq1Gjt2rK6//nr98ssvmjRpknr1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Vfv379dvf/tbp948LS1N119/verXr6/IyEgNHDhQ+/fvv+J+H3zwgdq2baugoCB16NBBn332mVPvC1Rb2anyLKgKAF7J6X+5Dx06pEaNGrnkzdeuXauxY8dq06ZNWrVqlS5cuKBevXqpoKCgwn2++eYbpaSk6IEHHtCOHTs0cOBADRw4ULt373ZJTcAVMVUeAHyGxTAM40qNXnzxRYcP+Oijj1a5mJ9++kmRkZFau3atbrnllnLbDB48WAUFBfr0009t22688UZdd911Dk3lz8vLU3h4uHJzcxUWFlblWmFS5U2VTz0mBYZ6rqYyCot+UbspKyVJe6f1VkiA03MiAKDWqanPb4f+hZwzZ45DB7NYLNUKQrm5uZKkiIiICtts3LhR48ePt9vWu3dvLV++vNz2VqtVVqvV9jwvL6/K9QFMlQcA3+JQEDp06FBN16GSkhKNGzdON910k6699toK2+Xk5CgqKspuW1RUlHJycsptn5aWpqlTp7q0VphU2XFBTJUHAK9XrdGdhmHIgW/WHDJ27Fjt3r1b7733nkuOVyo1NVW5ubm2x9GjR116fJhI2QVVCUEA4PWqFITefPNNdejQQcHBwQoODlbHjh311ltvVbmIhx9+WJ9++qlWr16tq6++utK20dHROn78uN2248ePKzo6utz2gYGBCgsLs3sATmNBVQDwSU4HoRdeeEFjxoxRv379tGTJEi1ZskR9+vTR6NGjHR5LVMowDD388MNatmyZvv76a7Vs2fKK+yQlJSk9Pd1u26pVq5SUlOTUewNOKdsbxLggAPAJTk8neemll/Tqq69q6NChtm133HGH2rdvr7/85S96/PHHHT7W2LFj9e677+rjjz9W/fr1beN8wsPDFRwcLEkaOnSomjZtqrS0NEnSY489pm7dumn27Nnq37+/3nvvPW3dulXz58939lSAKzOMiyHo0mU06A0CAJ/hdI9Qdna2kpOTL9uenJys7Oxsp4716quvKjc3V927d1dMTIzt8f7779vaZGVl2R03OTlZ7777rubPn69OnTrpww8/1PLlyysdYA1USelU+bSm9sto0BsEAD7D6R6huLg4LVmyRJMmTbLb/v777ys+Pt6pYzky0HrNmjWXbbv77rt19913O/VegNPKTpWP7sAyGgDgY5wOQlOnTtXgwYO1bt063XTTTZKkDRs2KD09XUuW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1q2NoKAgIyIiwujevbvx9ddfe6b4KirvfCXZXT9f/P2uynn7wu/3/fffbzRv3twICAgwGjdubNx22222MGAYvnmtDcP583bltbYYhmE4348EAADg/bx2jBAAAEB1EYQAAIBpEYQAAIBpEYQAAIBpEYQAAIBpEYQAAIBpEYQAAIBpEYQAAIBpEYQAuN3w4cM1cOBAj73/kCFDNH36dJccq6ioSC1atNDWrVtdcjwA7sWdpQG4lMViqfT1Z555Ro8//rgMw1CDBg3cU9Ql/v3vf6tHjx46cuSIQkNDXXLMl19+WcuWLbts8UsAtR9BCIBL5eTk2H5+//33NWXKFO3fv9+2LTQ01GUBpCpGjhypunXr6rXXXnPZMc+cOaPo6Ght375d7du3d9lxAdQ8vhoD4FLR0dG2R3h4uCwWi9220NDQy74a6969ux555BGNGzdODRs2VFRUlP7+97+roKBAI0aMUP369RUXF6fPP//c7r12796tvn37KjQ0VFFRURoyZIhOnjxZYW3FxcX68MMPNWDAALvtLVq00PTp03X//ferfv36atasmebPn297vaioSA8//LBiYmIUFBSk5s2bKy0tzfZ6w4YNddNNN+m9996r5p8eAHcjCAGoFd544w01atRI3377rR555BGNGTNGd999t5KTk7V9+3b16tVLQ4YMUWFhoSTp7Nmz6tGjhxITE7V161Z98cUXOn78uAYNGlThe+zcuVO5ubnq2rXrZa/Nnj1bXbt21Y4dO/THP/5RY8aMsfVkvfjii/rkk0+0ZMkS7d+/X++8845atGhht/8NN9ygf/3rX677AwHgFgQhALVCp06dNHnyZMXHxys1NVVBQUFq1KiRHnzwQcXHx2vKlCk6deqUdu7cKeniuJzExERNnz5dbdu2VWJiohYtWqTVq1frwIED5b7HkSNHVKdOHUVGRl72Wr9+/fTHP/5RcXFxmjhxoho1aqTVq1dLkrKyshQfH6+bb75ZzZs3180336yUlBS7/Zs0aaIjR464+E8FQE0jCAGoFTp27Gj7uU6dOrrqqqvUoUMH27aoqChJ0okTJyRdHPS8evVq25ij0NBQtW3bVpKUmZlZ7nucP39egYGB5Q7ovvT9S7/OK32v4cOHKyMjQwkJCXr00Uf15ZdfXrZ/cHCwrbcKgPeo6+kCAECS/P397Z5bLBa7baXhpaSkRJKUn5+vAQMGaMaMGZcdKyYmptz3aNSokQoLC1VUVKSAgIArvn/pe3Xu3FmHDh3S559/rq+++kqDBg1Sz5499eGHH9ranz59Wo0bN3b0dAHUEgQhAF6pc+fOWrp0qVq0aKG6dR37p+y6666TJO3du9f2s6PCwsI0ePBgDR48WH/4wx/Up08fnT59WhEREZIuDtxOTEx06pgAPI+vxgB4pbFjx+r06dNKSUnRli1blJmZqZUrV2rEiBEqLi4ud5/GjRurc+fOWr9+vVPv9cILL+gf//iHvvvuOx04cEAffPCBoqOj7e6D9K9//Uu9evWqzikB8ACCEACv1KRJE23YsEHFxcXq1auXOnTooHHjxqlBgwby86v4n7aRI0fqnXfeceq96tevr5kzZ6pr1666/vrrdfjwYX322We299m4caNyc3P1hz/8oVrnBMD9uKEiAFM5f/68EhIS9P777yspKcklxxw8eLA6deqkSZMmueR4ANyHHiEAphIcHKw333yz0hsvOqOoqEgdOnTQ448/7pLjAXAveoQAAIBp0SMEAABMiyAEAABMiyAEAABMiyAEAABMiyAEAABMiyAEAABMiyAEAABMiyAEAABMiyAEAABM6/8AlmJrNdWIpWQAAAAASUVORK5CYII=",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from qupulse.pulses.plotting import plot\n",
+ "import numpy as np\n",
+ "\n",
+ "parameters = dict(t=3,\n",
+ " v_0=1,\n",
+ " v_1=np.array([1.2, 2.5]))\n",
+ "\n",
+ "_ = plot(point_template, parameters, sample_rate=100)"
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/doc/source/examples/00RetrospectiveConstantChannelAddition.ipynb b/doc/source/examples/00RetrospectiveConstantChannelAddition.ipynb
new file mode 100644
index 000000000..0f934c535
--- /dev/null
+++ b/doc/source/examples/00RetrospectiveConstantChannelAddition.ipynb
@@ -0,0 +1,128 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# ParallelConstantChannelPulseTemplate\n",
+ "One reoccuring problem is to add a constant channel to an already existing possibly complex pulse. The setting in this example requires us to put a trigger pulse before the example pulse written in [03FreeInductionDecayExample](03FreeInductionDecayExample.ipynb). Unfortunately, the trigger pulse has to be played on a seperate marker channel that is not included in the example pulse. Therefore, we will add this channel to the pulse with the constant value 0.\n",
+ "\n",
+ "Let us start with loading the experiment and defining the trigger pulse"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Defined channels of loaded pulse: {'RFY', 'RFX'}\n",
+ "Defined channels of trigger pulse: {'RFY', 'RFX', 'Marker'}\n"
+ ]
+ }
+ ],
+ "source": [
+ "from qupulse.pulses import TablePT\n",
+ "from qupulse.serialization import FilesystemBackend, PulseStorage\n",
+ "\n",
+ "pulse_storage = PulseStorage(FilesystemBackend('./serialized_pulses'))\n",
+ "free_induction_decay = pulse_storage['free_induction_decay']\n",
+ "print('Defined channels of loaded pulse:', free_induction_decay.defined_channels)\n",
+ "\n",
+ "trig_pulse = TablePT({'RFX': [(0, 0), ('t_trig', 0)],\n",
+ " 'RFY': [(0, 0), ('t_trig', 0)],\n",
+ " 'Marker': [(0, 1), ('t_trig', 1)]})\n",
+ "print('Defined channels of trigger pulse:', trig_pulse.defined_channels)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If we now try to concatenate the pulses we get an error as they differ in their defined channels."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "ValueError(\"The subtemplates are defined for different channels: defined {'RFY', 'RFX', 'Marker'} vs. subtemplate {'RFY', 'RFX'}\")\n"
+ ]
+ }
+ ],
+ "source": [
+ "try:\n",
+ " experiment = trig_pulse @ free_induction_decay\n",
+ "except ValueError as err:\n",
+ " print(repr(err))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can now add an extra channel with a constant value to the `free_induction_decay` pulse. This allows us to concatenate the pulses."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from qupulse.pulses.multi_channel_pulse_template import ParallelConstantChannelPulseTemplate\n",
+ "extended_free_induction_decay = ParallelConstantChannelPulseTemplate(free_induction_decay, {'Marker': 0})\n",
+ "\n",
+ "experiment = trig_pulse @ extended_free_induction_decay"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Read example parameters from file and plot complete pulse"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import json\n",
+ "from qupulse.pulses.plotting import plot\n",
+ "\n",
+ "with open('parameters/free_induction_decay.json', 'r') as parameter_file:\n",
+ " example_values = json.load(parameter_file)\n",
+ "\n",
+ "_ = plot(experiment, {**example_values, 't_trig': 100, 'N_fid_steps': 2})"
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/doc/source/examples/00SimpleTablePulse.ipynb b/doc/source/examples/00SimpleTablePulse.ipynb
index 8f603d42e..6c7c313c5 100644
--- a/doc/source/examples/00SimpleTablePulse.ipynb
+++ b/doc/source/examples/00SimpleTablePulse.ipynb
@@ -65,791 +65,9 @@
"outputs": [
{
"data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjcAAAGwCAYAAABVdURTAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAA5f0lEQVR4nO3de1xUdeL/8TcgVwW8clHxCok3EMULamlqmvm12G+Z63dLNG3T1Uqpr34xV6vdlcrMtFzNyrXLmlqmtVYqkpdUzCulmZlXvADeQVBBYX5/9HO2WcFmcOAMh9fz8ZjHg/nMOWfeMxW8O+czn3GzWCwWAQAAmIS70QEAAACciXIDAABMhXIDAABMhXIDAABMhXIDAABMhXIDAABMhXIDAABMpZrRASpacXGxTp06JX9/f7m5uRkdBwAA2MFisejSpUuqX7++3N1vfW6mypWbU6dOKSwszOgYAACgDI4fP66GDRvecpsqV278/f0l/fLmBAQEGJwGAADYIzc3V2FhYda/47dS5crNjUtRAQEBlBsAACoZe6aUMKEYAACYCuUGAACYCuUGAACYSpWbcwMAMJeioiJdu3bN6BhwAi8vr9/8mLc9KDcAgErJYrEoKytLFy9eNDoKnMTd3V1NmzaVl5fXbR2HcgMAqJRuFJugoCD5+fmxMGsld2OR3czMTDVq1Oi2/nlSbgAAlU5RUZG12NSpU8foOHCSevXq6dSpU7p+/bo8PT3LfBwmFAMAKp0bc2z8/PwMTgJnunE5qqio6LaOQ7kBAFRaXIoyF2f986TcAAAAU6HcAAAAU6HcAADgIo4ePSo3Nzelp6cbHcUuPXv21Lhx44yOcRPKDQAAKDfr169X+/bt5e3trfDwcC1cuLDcn5NyAwAAysWRI0c0YMAA3X333UpPT9e4ceM0cuRIrV69ulyfl3IDADAFi8Wiy4XXDblZLBa7cxYXF+uVV15ReHi4vL291ahRI/3tb3+z2ebw4cO6++675efnp+joaKWlpVkfO3funIYMGaIGDRrIz89Pbdu21UcffWSzf8+ePfXUU09pwoQJql27tkJCQvT888/bbOPm5qZ33nlHv/vd7+Tn56eIiAh9/vnnNtvs3btX/fv3V40aNRQcHKxHH31UZ8+etfu1zps3T02bNtWMGTPUsmVLjR07Vg899JBmzpxp9zHKgkX8AACmcOVakVpNKd8zAqXZ92I/+XnZ9yc1KSlJb7/9tmbOnKnu3bsrMzNT+/fvt9nmueee06uvvqqIiAg999xzGjJkiA4ePKhq1arp6tWr6tChgyZOnKiAgAB98cUXevTRR9W8eXN16tTJeoz33ntPiYmJ+vbbb5WWlqZhw4apW7duuueee6zbvPDCC3rllVc0ffp0vfHGG/rDH/6gY8eOqXbt2rp48aJ69eqlkSNHaubMmbpy5YomTpyohx9+WF9//bVdrzUtLU19+vSxGevXr1+5z9Oh3AAAUEEuXbqkWbNm6c0331RCQoIkqXnz5urevbvNds8++6wGDBgg6ZcC0rp1ax08eFCRkZFq0KCBnn32Weu2Tz75pFavXq2lS5falJuoqChNnTpVkhQREaE333xTqampNuVm2LBhGjJkiCRp2rRpmj17trZt26Z7771Xb775pmJiYjRt2jTr9gsWLFBYWJgOHDigO+644zdfb1ZWloKDg23GgoODlZubqytXrsjX19eu981RlBsAgCn4enpo34v9DHtue/z4448qKChQ7969b7ldVFSU9efQ0FBJ0unTpxUZGamioiJNmzZNS5cu1cmTJ1VYWKiCgoKbVmv+9TFuHOf06dOlblO9enUFBARYt/nuu++0bt061ahR46Z8hw4dsqvcGIVyAwAwBTc3N7svDRnF3jMVv/5epRur9hYXF0uSpk+frlmzZun1119X27ZtVb16dY0bN06FhYWlHuPGcW4cw55t8vLyNHDgQL388ss35btRuH5LSEiIsrOzbcays7MVEBBQbmdtJMoNAAAVJiIiQr6+vkpNTdXIkSPLdIzNmzfrgQce0COPPCLpl9Jz4MABtWrVyplR1b59ey1btkxNmjRRtWplqwtxcXH68ssvbcZSUlIUFxfnjIil4tNSAABUEB8fH02cOFETJkzQ+++/r0OHDmnr1q1699137T5GRESEUlJStGXLFv3444964oknbjo74gxjxozR+fPnNWTIEG3fvl2HDh3S6tWrNXz4cLu/2HLUqFE6fPiwJkyYoP379+vvf/+7li5dqvHjxzs9769x5gYAgAr05z//WdWqVdOUKVN06tQphYaGatSoUXbvP3nyZB0+fFj9+vWTn5+f/vjHPyo+Pl45OTlOzVm/fn1t3rxZEydOVN++fVVQUKDGjRvr3nvvlbu7fedGmjZtqi++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AwIAADjC4TM3sbGx1p+DgoK0atUqpwYCAAC4HSziBwAATMWucnPvvfdq69atv7ndpUuX9PLLL2vOnDm3HQwAAKAs7LosNWjQID344IMKDAzUwIEDFRsbq/r168vHx0cXLlzQvn37tGnTJn355ZcaMGCApk+fXt65AQAASmRXuRkxYoQeeeQRffzxx1qyZInmz5+vnJwcSZKbm5tatWqlfv36afv27WrZsmW5BgYAALgVuycUe3t765FHHtEjjzwiScrJydGVK1dUp04deXp6lltAAAAAR5TpizOlXxbtCwwMdGYWAACA28anpQAAgKlQbgAAgKlQbgAAgKkYWm42btyogQMHqn79+nJzc9OKFStuuf369evl5uZ20y0rK6tiAgMAAJdXpnJz8eJFvfPOO0pKStL58+clSbt27dLJkycdOk5+fr6io6MdXvTvp59+UmZmpvUWFBTk0P4AAMC8HP601Pfff68+ffooMDBQR48e1eOPP67atWvr008/VUZGht5//327j9W/f3/179/f0QgKCgpSzZo1Hd4PAACYn8NnbhITEzVs2DD9/PPP8vHxsY7fd9992rhxo1PDlaZdu3YKDQ3VPffco82bN99y24KCAuXm5trcAACAeTlcbrZv364nnnjipvEGDRqU+9yX0NBQzZs3T8uWLdOyZcsUFhamnj17ateuXaXuk5ycbF2TJzAwUGFhYeWaEQAAGMvhy1Le3t4lnv04cOCA6tWr55RQpWnRooVatGhhvd+1a1cdOnRIM2fO1AcffFDiPklJSUpMTLTez83NpeAAAGBiDp+5uf/++/Xiiy/q2rVrkn75bqmMjAxNnDhRDz74oNMD/pZOnTrp4MGDpT7u7e2tgIAAmxsAADAvh8vNjBkzlJeXp6CgIF25ckU9evRQeHi4/P399be//a08Mt5Senq6QkNDK/x5AQCAa3L4slRgYKBSUlK0adMmff/998rLy1P79u3Vp08fh588Ly/P5qzLkSNHlJ6ertq1a6tRo0ZKSkrSyZMnrZ/Aev3119W0aVO1bt1aV69e1TvvvKOvv/5aa9ascfi5AQCAOZX5izO7d++u7t2739aT79ixQ3fffbf1/o25MQkJCVq4cKEyMzOVkZFhfbywsFDPPPOMTp48KT8/P0VFRWnt2rU2xwAAAFWbw+Vm9uzZJY67ubnJx8dH4eHhuuuuu+Th4fGbx+rZs6csFkupjy9cuNDm/oQJEzRhwgSH8gIAgKrF4XIzc+ZMnTlzRpcvX1atWrUkSRcuXJCfn59q1Kih06dPq1mzZlq3bh2fSgIAABXO4QnF06ZNU8eOHfXzzz/r3LlzOnfunA4cOKDOnTtr1qxZysjIUEhIiMaPH18eeQEAAG7J4TM3kydP1rJly9S8eXPrWHh4uF599VU9+OCDOnz4sF555RVDPhYOAADg8JmbzMxMXb9+/abx69evW1corl+/vi5dunT76QAAABzkcLm5++679cQTT2j37t3Wsd27d2v06NHq1auXJGnPnj1q2rSp81ICAADYyeFy8+6776p27drq0KGDvL295e3trdjYWNWuXVvvvvuuJKlGjRqaMWOG08MCAAD8Fofn3ISEhCglJUX79+/XgQMHJN38nU+sOwMAAIxS5kX8IiMjFRkZ6cwsAAAAt61M5ebEiRP6/PPPlZGRocLCQpvHXnvtNacEAwAAKAuHy01qaqruv/9+NWvWTPv371ebNm109OhRWSwWtW/fvjwyAgAA2M3hCcVJSUl69tlntWfPHvn4+GjZsmU6fvy4evTooUGDBpVHRgAAALs5XG5+/PFHDR06VJJUrVo1XblyRTVq1NCLL76ol19+2ekBAQAAHOFwualevbp1nk1oaKgOHTpkfezs2bPOSwYAAFAGDs+56dKlizZt2qSWLVvqvvvu0zPPPKM9e/bo008/VZcuXcojIwAAgN0cLjevvfaa8vLyJEkvvPCC8vLytGTJEkVERPBJKQAAYDiHy02zZs2sP1evXl3z5s1zaiAAAIDb4fCcm2bNmuncuXM3jV+8eNGm+AAAABjB4XJz9OhRFRUV3TReUFCgkydPOiUUAABAWdl9Werzzz+3/rx69WoFBgZa7xcVFSk1NVVNmjRxajgAAABH2V1u4uPjJUlubm5KSEiweczT01NNmjThm8ABAIDh7C43xcXFkqSmTZtq+/btqlu3brmFAgAAKCuHPy115MiR8sgBAADgFHaVm9mzZ9t9wKeeeqrMYQAAAG6XXeVm5syZdh3Mzc2NcgMAAAxlV7nhUhQAAKgsHF7n5tcsFossFouzsgAAANy2MpWb999/X23btpWvr698fX0VFRWlDz74wNnZAAAAHFamL87885//rLFjx6pbt26SpE2bNmnUqFE6e/asxo8f7/SQAAAA9nK43LzxxhuaO3euhg4dah27//771bp1az3//POUGwAAYCiHL0tlZmaqa9euN4137dpVmZmZTgkFAABQVg6Xm/DwcC1duvSm8SVLligiIsIpoQAAAMrK4ctSL7zwggYPHqyNGzda59xs3rxZqampJZYeAACAimT3mZu9e/dKkh588EF9++23qlu3rlasWKEVK1aobt262rZtm373u9+VW1AAAAB72H3mJioqSh07dtTIkSP1+9//Xh9++GF55gIAACgTu8/cbNiwQa1bt9Yzzzyj0NBQDRs2TN988015ZgMAAHCY3eXmzjvv1IIFC5SZmak33nhDR44cUY8ePXTHHXfo5ZdfVlZWVnnmBAAAsIvDn5aqXr26hg8frg0bNujAgQMaNGiQ5syZo0aNGun+++8vj4wAAAB2u63vlgoPD9ekSZM0efJk+fv764svvnBWLgAAgDJx+KPgN2zcuFELFizQsmXL5O7urocfflgjRoxwZjYAAACHOVRuTp06pYULF2rhwoU6ePCgunbtqtmzZ+vhhx9W9erVyysjAACA3ey+LNW/f381btxYb7zxhn73u9/pxx9/1KZNmzR8+PAyF5uNGzdq4MCBql+/vtzc3LRixYrf3Gf9+vVq3769vL29FR4eroULF5bpuQEAgDnZXW48PT31ySef6MSJE3r55ZfVokWL237y/Px8RUdHa86cOXZtf+TIEQ0YMEB333230tPTNW7cOI0cOVKrV6++7SwAAMAc7L4s9fnnnzv9yfv376/+/fvbvf28efPUtGlTzZgxQ5LUsmVLbdq0STNnzlS/fv2cng8AAFQ+t/VpqYqWlpamPn362Iz169dPaWlppe5TUFCg3NxcmxsAADCvSlVusrKyFBwcbDMWHBys3NxcXblypcR9kpOTFRgYaL2FhYVVRFQAAGCQSlVuyiIpKUk5OTnW2/Hjx42OBAAAylGZ17kxQkhIiLKzs23GsrOzFRAQIF9f3xL38fb2lre3d0XEAwAALqBSnbmJi4tTamqqzVhKSori4uIMSgQAAFyNoeUmLy9P6enpSk9Pl/TLR73T09OVkZEh6ZdLSkOHDrVuP2rUKB0+fFgTJkzQ/v379fe//11Lly7V+PHjjYgPAABckKHlZseOHYqJiVFMTIwkKTExUTExMZoyZYokKTMz01p0JKlp06b64osvlJKS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2rXTqlWrrJOGMzIy5O7+7xNMXbt21aJFizR58mRNmjRJERERWrFihdq0aWPUSwAAAC7E8HVuKporrXMDAADs48jf78r3+S4AAIBboNwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTqWZ0gIpmsVgkSbm5uQYnAQAA9rrxd/vG3/FbqXLl5tKlS5KksLAwg5MAAABHXbp0SYGBgbfcxs1iTwUykeLiYp06dUr+/v5yc3MzOo5yc3MVFham48ePKyAgwOg4LoX3pmS8L6XjvSkd703peG9K50rvjcVi0aVLl1S/fn25u996Vk2VO3Pj7u6uhg0bGh3jJgEBAYb/i+OqeG9KxvtSOt6b0vHelI73pnSu8t781hmbG5hQDAAATIVyAwAATIVyYzBvb29NnTpV3t7eRkdxObw3JeN9KR3vTel4b0rHe1O6yvreVLkJxQAAwNw4cwMAAEyFcgMAAEyFcgMAAEyFcgMAAEyFcmOgOXPmqEmTJvLx8VHnzp21bds2oyO5hI0bN2rgwIGqX7++3NzctGLFCqMjuYTk5GR17NhR/v7+CgoKUnx8vH766SejY7mEuXPnKioqyrrQWFxcnL766iujY7mcl156SW5ubho3bpzRUVzC888/Lzc3N5tbZGSk0bFcwsmTJ/XII4+oTp068vX1Vdu2bbVjxw6jY9mNcmOQJUuWKDExUVOnTtWuXbsUHR2tfv366fTp00ZHM1x+fr6io6M1Z84co6O4lA0bNmjMmDHaunWrUlJSdO3aNfXt21f5+flGRzNcw4YN9dJLL2nnzp3asWOHevXqpQceeEA//PCD0dFcxvbt2/XWW28pKirK6CgupXXr1srMzLTeNm3aZHQkw124cEHdunWTp6envvrqK+3bt08zZsxQrVq1jI5mPwsM0alTJ8uYMWOs94uKiiz169e3JCcnG5jK9UiyLF++3OgYLun06dMWSZYNGzYYHcUl1apVy/LOO+8YHcMlXLp0yRIREWFJSUmx9OjRw/L0008bHcklTJ061RIdHW10DJczceJES/fu3Y2OcVs4c2OAwsJC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",
"text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -857,7 +75,6 @@
}
],
"source": [
- "%matplotlib notebook\n",
"from qupulse.pulses.plotting import plot\n",
"\n",
"_ = plot(template, sample_rate=100)"
@@ -869,7 +86,7 @@
"source": [
"Alright, we got what we wanted. \n",
"\n",
- "Note that the time domain in pulse defintions does not correspond to any fixed real world time unit. The mapping from a single time unit in a pulse definition to real time in execution is made by setting a sample rate when instantiating pulses for execution from the pulse templates. For more on this, see [Instantiating Pulses](06CreatePrograms.ipynb).\n",
+ "Note that the time domain in pulse defintions does not correspond to any fixed real world time unit. The mapping from a single time unit in a pulse definition to real time in execution is made by setting a sample rate when instantiating pulses for execution from the pulse templates. For more on this, see [Instantiating Pulses](02CreatePrograms.ipynb).\n",
"\n",
"## Introducing Parameters\n",
"Now we want to make the template parameterizable. This allows us to reuse the template for pulses with similar structure. Say we would like to have the same pulse, but the intermediate linear interpolation part should last 4 units of time instead of only 2. Instead of creating another template with hardcoded values, we instruct the `TablePulseTemplate` instance to rely on parameters."
@@ -905,7 +122,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "{'vb', 'ta', 'tend', 'tb', 'va'}\n"
+ "{'vb', 'ta', 'va', 'tb', 'tend'}\n"
]
}
],
@@ -927,791 +144,9 @@
"outputs": [
{
"data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjcAAAGwCAYAAABVdURTAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAA5f0lEQVR4nO3de1xUdeL/8TcgVwW8clHxCok3EMULamlqmvm12G+Z63dLNG3T1Uqpr34xV6vdlcrMtFzNyrXLmlqmtVYqkpdUzCulmZlXvADeQVBBYX5/9HO2WcFmcOAMh9fz8ZjHg/nMOWfeMxW8O+czn3GzWCwWAQAAmIS70QEAAACciXIDAABMhXIDAABMhXIDAABMhXIDAABMhXIDAABMhXIDAABMpZrRASpacXGxTp06JX9/f7m5uRkdBwAA2MFisejSpUuqX7++3N1vfW6mypWbU6dOKSwszOgYAACgDI4fP66GDRvecpsqV278/f0l/fLmBAQEGJwGAADYIzc3V2FhYda/47dS5crNjUtRAQEBlBsAACoZe6aUMKEYAACYCuUGAACYCuUGAACYSpWbcwMAMJeioiJdu3bN6BhwAi8vr9/8mLc9KDcAgErJYrEoKytLFy9eNDoKnMTd3V1NmzaVl5fXbR2HcgMAqJRuFJugoCD5+fmxMGsld2OR3czMTDVq1Oi2/nlSbgAAlU5RUZG12NSpU8foOHCSevXq6dSpU7p+/bo8PT3LfBwmFAMAKp0bc2z8/PwMTgJnunE5qqio6LaOQ7kBAFRaXIoyF2f986TcAAAAU6HcAAAAU6HcAADgIo4ePSo3Nzelp6cbHcUuPXv21Lhx44yOcRPKDQAAKDfr169X+/bt5e3trfDwcC1cuLDcn5NyAwAAysWRI0c0YMAA3X333UpPT9e4ceM0cuRIrV69ulyfl3IDADAFi8Wiy4XXDblZLBa7cxYXF+uVV15ReHi4vL291ahRI/3tb3+z2ebw4cO6++675efnp+joaKWlpVkfO3funIYMGaIGDRrIz89Pbdu21UcffWSzf8+ePfXUU09pwoQJql27tkJCQvT888/bbOPm5qZ33nlHv/vd7+Tn56eIiAh9/vnnNtvs3btX/fv3V40aNRQcHKxHH31UZ8+etfu1zps3T02bNtWMGTPUsmVLjR07Vg899JBmzpxp9zHKgkX8AACmcOVakVpNKd8zAqXZ92I/+XnZ9yc1KSlJb7/9tmbOnKnu3bsrMzNT+/fvt9nmueee06uvvqqIiAg999xzGjJkiA4ePKhq1arp6tWr6tChgyZOnKiAgAB98cUXevTRR9W8eXN16tTJeoz33ntPiYmJ+vbbb5WWlqZhw4apW7duuueee6zbvPDCC3rllVc0ffp0vfHGG/rDH/6gY8eOqXbt2rp48aJ69eqlkSNHaubMmbpy5YomTpyohx9+WF9//bVdrzUtLU19+vSxGevXr1+5z9Oh3AAAUEEuXbqkWbNm6c0331RCQoIkqXnz5urevbvNds8++6wGDBgg6ZcC0rp1ax08eFCRkZFq0KCBnn32Weu2Tz75pFavXq2lS5falJuoqChNnTpVkhQREaE333xTqampNuVm2LBhGjJkiCRp2rRpmj17trZt26Z7771Xb775pmJiYjRt2jTr9gsWLFBYWJgOHDigO+644zdfb1ZWloKDg23GgoODlZubqytXrsjX19eu981RlBsAgCn4enpo34v9DHtue/z4448qKChQ7969b7ldVFSU9efQ0FBJ0unTpxUZGamioiJNmzZNS5cu1cmTJ1VYWKiCgoKbVmv+9TFuHOf06dOlblO9enUFBARYt/nuu++0bt061ahR46Z8hw4dsqvcGIVyAwAwBTc3N7svDRnF3jMVv/5epRur9hYXF0uSpk+frlmzZun1119X27ZtVb16dY0bN06FhYWlHuPGcW4cw55t8vLyNHDgQL388ss35btRuH5LSEiIsrOzbcays7MVEBBQbmdtJMoNAAAVJiIiQr6+vkpNTdXIkSPLdIzNmzfrgQce0COPPCLpl9Jz4MABtWrVyplR1b59ey1btkxNmjRRtWplqwtxcXH68ssvbcZSUlIUFxfnjIil4tNSAABUEB8fH02cOFETJkzQ+++/r0OHDmnr1q1699137T5GRESEUlJStGXLFv3444964oknbjo74gxjxozR+fPnNWTIEG3fvl2HDh3S6tWrNXz4cLu/2HLUqFE6fPiwJkyYoP379+vvf/+7li5dqvHjxzs9769x5gYAgAr05z//WdWqVdOUKVN06tQphYaGatSoUXbvP3nyZB0+fFj9+vWTn5+f/vjHPyo+Pl45OTlOzVm/fn1t3rxZEydOVN++fVVQUKDGjRvr3nvvlbu7fedGmjZtqi++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AwIAADjC4TM3sbGx1p+DgoK0atUqpwYCAAC4HSziBwAATMWucnPvvfdq69atv7ndpUuX9PLLL2vOnDm3HQwAAKAs7LosNWjQID344IMKDAzUwIEDFRsbq/r168vHx0cXLlzQvn37tGnTJn355ZcaMGCApk+fXt65AQAASmRXuRkxYoQeeeQRffzxx1qyZInmz5+vnJwcSZKbm5tatWqlfv36afv27WrZsmW5BgYAALgVuycUe3t765FHHtEjjzwiScrJydGVK1dUp04deXp6lltAAAAAR5TpizOlXxbtCwwMdGYWAACA28anpQAAgKlQbgAAgKlQbgAAgKkYWm42btyogQMHqn79+nJzc9OKFStuuf369evl5uZ20y0rK6tiAgMAAJdXpnJz8eJFvfPOO0pKStL58+clSbt27dLJkycdOk5+fr6io6MdXvTvp59+UmZmpvUWFBTk0P4AAMC8HP601Pfff68+ffooMDBQR48e1eOPP67atWvr008/VUZGht5//327j9W/f3/179/f0QgKCgpSzZo1Hd4PAACYn8NnbhITEzVs2DD9/PPP8vHxsY7fd9992rhxo1PDlaZdu3YKDQ3VPffco82bN99y24KCAuXm5trcAACAeTlcbrZv364nnnjipvEGDRqU+9yX0NBQzZs3T8uWLdOyZcsUFhamnj17ateuXaXuk5ycbF2TJzAwUGFhYeWaEQAAGMvhy1Le3t4lnv04cOCA6tWr55RQpWnRooVatGhhvd+1a1cdOnRIM2fO1AcffFDiPklJSUpMTLTez83NpeAAAGBiDp+5uf/++/Xiiy/q2rVrkn75bqmMjAxNnDhRDz74oNMD/pZOnTrp4MGDpT7u7e2tgIAAmxsAADAvh8vNjBkzlJeXp6CgIF25ckU9evRQeHi4/P399be//a08Mt5Senq6QkNDK/x5AQCAa3L4slRgYKBSUlK0adMmff/998rLy1P79u3Vp08fh588Ly/P5qzLkSNHlJ6ertq1a6tRo0ZKSkrSyZMnrZ/Aev3119W0aVO1bt1aV69e1TvvvKOvv/5aa9ascfi5AQCAOZX5izO7d++u7t2739aT79ixQ3fffbf1/o25MQkJCVq4cKEyMzOVkZFhfbywsFDPPPOMTp48KT8/P0VFRWnt2rU2xwAAAFWbw+Vm9uzZJY67ubnJx8dH4eHhuuuuu+Th4fGbx+rZs6csFkupjy9cuNDm/oQJEzRhwgSH8gIAgKrF4XIzc+ZMnTlzRpcvX1atWrUkSRcuXJCfn59q1Kih06dPq1mzZlq3bh2fSgIAABXO4QnF06ZNU8eOHfXzzz/r3LlzOnfunA4cOKDOnTtr1qxZysjIUEhIiMaPH18eeQEAAG7J4TM3kydP1rJly9S8eXPrWHh4uF599VU9+OCDOnz4sF555RVDPhYOAADg8JmbzMxMXb9+/abx69evW1corl+/vi5dunT76QAAABzkcLm5++679cQTT2j37t3Wsd27d2v06NHq1auXJGnPnj1q2rSp81ICAADYyeFy8+6776p27drq0KGDvL295e3trdjYWNWuXVvvvvuuJKlGjRqaMWOG08MCAAD8Fofn3ISEhCglJUX79+/XgQMHJN38nU+sOwMAAIxS5kX8IiMjFRkZ6cwsAAAAt61M5ebEiRP6/PPPlZGRocLCQpvHXnvtNacEAwAAKAuHy01qaqruv/9+NWvWTPv371ebNm109OhRWSwWtW/fvjwyAgAA2M3hCcVJSUl69tlntWfPHvn4+GjZsmU6fvy4evTooUGDBpVHRgAAALs5XG5+/PFHDR06VJJUrVo1XblyRTVq1NCLL76ol19+2ekBAQAAHOFwualevbp1nk1oaKgOHTpkfezs2bPOSwYAAFAGDs+56dKlizZt2qSWLVvqvvvu0zPPPKM9e/bo008/VZcuXcojIwAAgN0cLjevvfaa8vLyJEkvvPCC8vLytGTJEkVERPBJKQAAYDiHy02zZs2sP1evXl3z5s1zaiAAAIDb4fCcm2bNmuncuXM3jV+8eNGm+AAAABjB4XJz9OhRFRUV3TReUFCgkydPOiUUAABAWdl9Werzzz+3/rx69WoFBgZa7xcVFSk1NVVNmjRxajgAAABH2V1u4uPjJUlubm5KSEiweczT01NNmjThm8ABAIDh7C43xcXFkqSmTZtq+/btqlu3brmFAgAAKCuHPy115MiR8sgBAADgFHaVm9mzZ9t9wKeeeqrMYQAAAG6XXeVm5syZdh3Mzc2NcgMAAAxlV7nhUhQAAKgsHF7n5tcsFossFouzsgAAANy2MpWb999/X23btpWvr698fX0VFRWlDz74wNnZAAAAHFamL87885//rLFjx6pbt26SpE2bNmnUqFE6e/asxo8f7/SQAAAA9nK43LzxxhuaO3euhg4dah27//771bp1az3//POUGwAAYCiHL0tlZmaqa9euN4137dpVmZmZTgkFAABQVg6Xm/DwcC1duvSm8SVLligiIsIpoQAAAMrK4ctSL7zwggYPHqyNGzda59xs3rxZqampJZYeAACAimT3mZu9e/dKkh588EF9++23qlu3rlasWKEVK1aobt262rZtm373u9+VW1AAAAB72H3mJioqSh07dtTIkSP1+9//Xh9++GF55gIAACgTu8/cbNiwQa1bt9Yzzzyj0NBQDRs2TN988015ZgMAAHCY3eXmzjvv1IIFC5SZmak33nhDR44cUY8ePXTHHXfo5ZdfVlZWVnnmBAAAsIvDn5aqXr26hg8frg0bNujAgQMaNGiQ5syZo0aNGun+++8vj4wAAAB2u63vlgoPD9ekSZM0efJk+fv764svvnBWLgAAgDJx+KPgN2zcuFELFizQsmXL5O7urocfflgjRoxwZjYAAACHOVRuTp06pYULF2rhwoU6ePCgunbtqtmzZ+vhhx9W9erVyysjAACA3ey+LNW/f381btxYb7zxhn73u9/pxx9/1KZNmzR8+PAyF5uNGzdq4MCBql+/vtzc3LRixYrf3Gf9+vVq3769vL29FR4eroULF5bpuQEAgDnZXW48PT31ySef6MSJE3r55ZfVokWL237y/Px8RUdHa86cOXZtf+TIEQ0YMEB333230tPTNW7cOI0cOVKrV6++7SwAAMAc7L4s9fnnnzv9yfv376/+/fvbvf28efPUtGlTzZgxQ5LUsmVLbdq0STNnzlS/fv2cng8AAFQ+t/VpqYqWlpamPn362Iz169dPaWlppe5TUFCg3NxcmxsAADCvSlVusrKyFBwcbDMWHBys3NxcXblypcR9kpOTFRgYaL2FhYVVRFQAAGCQSlVuyiIpKUk5OTnW2/Hjx42OBAAAylGZ17kxQkhIiLKzs23GsrOzFRAQIF9f3xL38fb2lre3d0XEAwAALqBSnbmJi4tTamqqzVhKSori4uIMSgQAAFyNoeUmLy9P6enpSk9Pl/TLR73T09OVkZEh6ZdLSkOHDrVuP2rUKB0+fFgTJkzQ/v379fe//11Lly7V+PHjjYgPAABckKHlZseOHYqJiVFMTIwkKTExUTExMZoyZYokKTMz01p0JKlp06b64osvlJKS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2rXTqlWrrJOGMzIy5O7+7xNMXbt21aJFizR58mRNmjRJERERWrFihdq0aWPUSwAAAC7E8HVuKporrXMDAADs48jf78r3+S4AAIBboNwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTqWZ0gIpmsVgkSbm5uQYnAQAA9rrxd/vG3/FbqXLl5tKlS5KksLAwg5MAAABHXbp0SYGBgbfcxs1iTwUykeLiYp06dUr+/v5yc3MzOo5yc3MVFham48ePKyAgwOg4LoX3pmS8L6XjvSkd703peG9K50rvjcVi0aVLl1S/fn25u996Vk2VO3Pj7u6uhg0bGh3jJgEBAYb/i+OqeG9KxvtSOt6b0vHelI73pnSu8t781hmbG5hQDAAATIVyAwAATIVyYzBvb29NnTpV3t7eRkdxObw3JeN9KR3vTel4b0rHe1O6yvreVLkJxQAAwNw4cwMAAEyFcgMAAEyFcgMAAEyFcgMAAEyFcmOgOXPmqEmTJvLx8VHnzp21bds2oyO5hI0bN2rgwIGqX7++3NzctGLFCqMjuYTk5GR17NhR/v7+CgoKUnx8vH766SejY7mEuXPnKioqyrrQWFxcnL766iujY7mcl156SW5ubho3bpzRUVzC888/Lzc3N5tbZGSk0bFcwsmTJ/XII4+oTp068vX1Vdu2bbVjxw6jY9mNcmOQJUuWKDExUVOnTtWuXbsUHR2tfv366fTp00ZHM1x+fr6io6M1Z84co6O4lA0bNmjMmDHaunWrUlJSdO3aNfXt21f5+flGRzNcw4YN9dJLL2nnzp3asWOHevXqpQceeEA//PCD0dFcxvbt2/XWW28pKirK6CgupXXr1srMzLTeNm3aZHQkw124cEHdunWTp6envvrqK+3bt08zZsxQrVq1jI5mPwsM0alTJ8uYMWOs94uKiiz169e3JCcnG5jK9UiyLF++3OgYLun06dMWSZYNGzYYHcUl1apVy/LOO+8YHcMlXLp0yRIREWFJSUmx9OjRw/L0008bHcklTJ061RIdHW10DJczceJES/fu3Y2OcVs4c2OAwsJC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",
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -1741,791 +176,9 @@
"outputs": [
{
"data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjcAAAGwCAYAAABVdURTAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAA7n0lEQVR4nO3deXxNd+L/8XcSsiAJEdkIQlKxC7EktJZqFV+tmVaNX1Vs3SZapB0aVUbnWymqShmqqroZlFJttaqppVFqTUsVtUZJYk8IEnLv749+ZSZzE3KTm5zkeD0fj/t4uJ/Puee+L628nXs+5zhZrVarAAAATMLZ6AAAAACORLkBAACmQrkBAACmQrkBAACmQrkBAACmQrkBAACmQrkBAACmUsnoAGXNYrHo1KlT8vT0lJOTk9FxAABAEVitVl26dElBQUFydr71sZk7rtycOnVKwcHBRscAAADFcOLECdWpU+eW29xx5cbT01PSH785Xl5eBqcBAABFkZmZqeDg4Lyf47dyx5Wbm19FeXl5UW4AAKhginJKCScUAwAAU6HcAAAAU6HcAAAAU7njzrkBAJhLbm6url+/bnQMOICrq+ttl3kXBeUGAFAhWa1WpaWl6eLFi0ZHgYM4OzsrJCRErq6uJdoP5QYAUCHdLDZ+fn6qUqUKF2at4G5eZDc1NVV169Yt0Z8n5QYAUOHk5ubmFZuaNWsaHQcOUqtWLZ06dUo3btxQ5cqVi70fTigGAFQ4N8+xqVKlisFJ4Eg3v47Kzc0t0X4oNwCACouvoszFUX+elBsAAGAqlBsAAGAqlBsAAMqJY8eOycnJScnJyUZHKZIuXbpo1KhRRsewQbkBAAClZsOGDWrdurXc3NwUGhqqRYsWlfp7Um4AAECpOHr0qHr37q2uXbsqOTlZo0aN0vDhw7V27dpSfV/KDQDAFKxWq67k3DDkYbVai5zTYrFo6tSpCg0NlZubm+rWratXX3013zZHjhxR165dVaVKFbVs2VJbtmzJmzt37pwGDBig2rVrq0qVKmrevLn+9a9/5Xt9ly5d9Nxzz2nMmDHy8fFRQECA/v73v+fbxsnJSQsWLNCf/vQnValSRWFhYVq9enW+bfbu3auePXuqWrVq8vf31+OPP66zZ88W+bPOmzdPISEhmj59uho3bqwRI0bokUce0YwZM4q8j+LgIn4AAFO4ej1XTSaU7hGBwux7pYequBbtR2p8fLzeeecdzZgxQ506dVJqaqr279+fb5uXXnpJr7/+usLCwvTSSy9pwIABOnTokCpVqqRr166pTZs2Gjt2rLy8vPTll1/q8ccfV8OGDdWuXbu8fbz//vuKi4vTjz/+qC1btmjw4MHq2LGj7rvvvrxtJk2apKlTp2ratGl666239Nhjj+n48ePy8fHRxYsX1a1bNw0fPlwzZszQ1atXNXbsWD366KP67rvvivRZt2zZou7du+cb69GjR6mfp0O5AQCgjFy6dEkzZ87U7NmzFRMTI0lq2LChOnXqlG+7F154Qb1795b0RwFp2rSpDh06pPDwcNWuXVsvvPBC3rbPPvus1q5dq2XLluUrNy1atNDEiRMlSWFhYZo9e7YSExPzlZvBgwdrwIABkqTJkydr1qxZ2rZtmx544AHNnj1bERERmjx5ct72CxcuVHBwsA4ePKi77rrrtp83LS1N/v7++cb8/f2VmZmpq1evysPDo0i/b/ai3AAATMGjsov2vdLDsPcuil9//VXZ2dm69957b7ldixYt8n4dGBgoSTp9+rTCw8OVm5uryZMna9myZTp58qRycnKUnZ1tc7Xm/9zHzf2cPn260G2qVq0qLy+vvG1++uknrV+/XtWqVbPJd/jw4SKVG6NQbgAApuDk5FTkr4aMUtQjFf95X6WbV+21WCySpGnTpmnmzJl688031bx5c1WtWlWjRo1STk5Oofu4uZ+b+yjKNpcvX1afPn00ZcoUm3w3C9ftBAQEKD09Pd9Yenq6vLy8Su2ojUS5AQCgzISFhcnDw0OJiYkaPnx4sfaxefNmPfTQQxo4cKCkP0rPwYMH1aRJE0dGVevWrbVixQrVr19flSoVry5ERUVpzZo1+cbWrVunqKgoR0QsFKulAAAoI+7u7ho7dqzGjBmjDz74QIcPH9bWrVv17rvvFnkfYWFhWrdunX744Qf9+uuveuqpp2yOjjhCbGyszp8/rwEDBmj79u06fPiw1q5dqyFDhhT5xpZPP/20jhw5ojFjxmj//v365z//qWXLlmn06NEOz/ufOHIDAEAZevnll1WpUiVNmDBBp06dUmBgoJ5++ukiv378+PE6cuSIevTooSpVqujJJ59U3759lZGR4dCcQUFB2rx5s8aOHav7779f2dnZqlevnh544AE5Oxft2EhISIi+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AHYqF+VmxIgR+uKLL7Rp0ybVqVPnltsGBAQoPT0931h6eroCAgJKMyIAAKggDP1aymq1asSIEVq5cqW+++47hYSE3PY1UVFRSkxMzDe2bt06RUVFlVZMAABQgRh65CY2NlaLFy/WZ599Jk9Pz7zzZry9veXh4SFJGjRokGrXrq2EhARJ0siRI9W5c2dNnz5dvXv31pIlS7Rjxw7Nnz/fsM8BAADKD0OP3MydO1cZGRnq0qWLAgMD8x5Lly7N2yYlJUWpqal5z6Ojo7V48WLNnz9fLVu21PLly7Vq1apbnoQMAADuHIYeuSnKJXY2bNhgM9avXz/169evFBIBAICKzu5yk52drR9//FHHjx/XlStXVKtWLUVERBTpfBkAAIDSVuRys3nzZs2cOVOff/65rl+/nndezPnz55Wdna0GDRroySef1NNPPy1PT8/SzAwAAFCoIp1z8+CDD6p///6qX7++vvnmG126dEnnzp3T77//ritXrui3337T+PHjlZiYqLvuukvr1q0r7dwAAAAFKtKRm969e2vFihWqXLlygfMNGjRQgwYNFBMTo3379uU7ARgAAKAsFancPPXUU0XeYZMmTdSkSZNiBwIAACiJcnNvKQAAAEdwWLmJiYlRt27dHLU7AACAYnHYdW5q164tZ2cOBAEAAGM5rNxMnjzZUbsCAAAoNg61AAAAU7H7yM3QoUNvOb9w4cJihwEAACgpu8vNhQsX8j2/fv269u7dq4sXL3JCMQAAMJzd5WblypU2YxaLRc8884waNmzokFAAAADF5ZBzbpydnRUXF6cZM2Y4YncAAADF5rATig8fPqwbN244ancAAADFYvfXUnFxcfmeW61Wpaam6ssvv1RMTIzDggEAABSH3eVm9+7d+Z47OzurVq1amj59+m1XUgEAAJQ2u8vN+vXrSyMHAACAQ3ARPwAAYCoOKzfjxo3jaykAAGA4h91b6uTJkzpx4oSjdgcAAFAsDis377//vqN2BQAAUGyccwMAAEylWEdusrKytHHjRqWkpCgnJyff3HPPPeeQYAAAAMVRrOvc9OrVS1euXFFWVpZ8fHx09uxZValSRX5+fpQbAABgKLu/lho9erT69OmjCxcuyMPDQ1u3btXx48fVpk0bvf7666WREQAAoMjsLjfJycl6/vnn5ezsLBcXF2VnZys4OFhTp07VuHHjSiMjAABAkdldbipXrixn5z9e5ufnp5SUFEmSt7c3S8EBAIDh7D7nJiIiQtu3b1dYWJg6d+6sCRMm6OzZs/rwww/VrFmz0sgIAABQZHYfuZk8ebICAwMlSa+++qpq1KihZ555RmfOnNH8+fMdHhAAAMAedh+5iYyMzPu1n5+fvv76a4cGAgAAKAku4gcAAEylSOXmgQce0NatW2+73aVLlzRlyhTNmTOnxMEAAACKo0hfS/Xr108PP/ywvL291adPH0VGRiooKEju7u66cOGC9u3bp6SkJK1Zs0a9e/fWtGnTSjs3AABAgYpUboYNG6aBAwfqk08+0dKlSzV//nxlZGRIkpycnNSkSRP16NFD27dvV+PGjUs1MAAAwK0U+YRiNzc3DRw4UAMHDpQkZWRk6OrVq6pZs6YqV65cagEBAADsUawbZ0p/XLTP29vbkVkAAABKjNVSAADAVCg3AADAVCg3AADAVAwtN5s2bVKfPn0UFBQkJycnrVq16pbbb9iwQU5OTjaPtLS0sgkMAADKvWKVm4sXL2rBggWKj4/X+fPnJUm7du3SyZMn7dpPVlaWWrZsafdF/w4cOKDU1NS8h5+fn12vBwAA5mX3aqmff/5Z3bt3l7e3t44dO6YnnnhCPj4++vTTT5WSkqIPPvigyPvq2bOnevbsaW8E+fn5qXr16na/DgAAmJ/dR27i4uI0ePBg/fbbb3J3d88b79WrlzZt2uTQcIVp1aqVAgMDdd9992nz5s233DY7O1uZmZn5HgAAwLzsLjfbt2/XU089ZTNeu3btUj/3JTAwUPPmzdOKFSu0YsUKBQcHq0uXLtq1a1ehr0lISMi7Jo+3t7eCg4NLNSMAADCW3V9Lubm5FXj04+DBg6pVq5ZDQhWmUaNGatSoUd7z6OhoHT58WDNmzNCHH35Y4Gvi4+MVFxeX9zwzM5OCAwCAidl95ObBBx/UK6+8ouvXr0v6495SKSkpGjt2rB5++GGHB7yddu3a6dChQ4XOu7m5ycvLK98DAACYl93lZvr06bp8+bL8/Px09epVde7cWaGhofL09NSrr75aGhlvKTk5WYGBgWX+vgAAoHyy+2spb29vrVu3TklJSfr55591+fJltW7dWt27d7f7zS9fvpzvqMvRo0eVnJwsHx8f1a1bV/Hx8Tp58mTeCqw333xTISEhatq0qa5du6YFCxbou+++0zfffGP3ewMAAHMq9o0zO3XqpE6dOpXozXfs2KGuXbvmPb95bkxMTIwWLVqk1NRUpaSk5M3n5OTo+eef18mTJ1WlShW1aNFC3377bb59AACAO5vd5WbWrFkFjjs5Ocnd3V2hoaG655575OLictt9denSRVartdD5RYsW5Xs+ZswYjRkzxq68AADgzmJ3uZkxY4bOnDmjK1euqEaNGpKkCxcuqEqVKqpWrZpOnz6tBg0aaP369axKAgAAZc7uE4onT56stm3b6rffftO5c+d07tw5HTx4UO3bt9fMmTOVkpKigIAAjR49ujTyAgAA3JLdR27Gjx+vFStWqGHDhnljoaGhev311/Xwww/ryJEjmjp1qiHLwgEAAOw+cpOamqobN27YjN+4cSPvCsVBQUG6dOlSydMBAADYye5y07VrVz311FPavXt33tju3bv1zDPPqFu3bpKkPXv2KCQkxHEpAQAAisjucvPuu+/Kx8dHbdq0kZubm9zc3BQZGSkfHx+9++67kqRq1app+vTpDg8LAABwO3afcxMQEKB169Zp//79OnjwoCTbez5x3RkAAGCUYl/ELzw8XOHh4Y7MAgAAUGLFKje///67Vq9erZSUFOXk5OSbe+ONNxwSDAAAoDjsLjeJiYl68MEH1aBBA+3fv1/NmjXTsWPHZLVa1bp169LICAAAUGR2n1AcHx+vF154QXv27JG7u7tWrFihEydOqHPnzurXr19pZAQAACgyu8vNr7/+qkGDBkmSKlWqpKtXr6patWp65ZVXNGXKFIcHBAAAsIfd5aZq1ap559kEBgbq8OHDeXNnz551XDIAAIBisPucmw4dOigpKUmNGzdWr1699Pzzz2vPnj369NNP1aFDh9LICAAAUGR2l5s33nhDly9fliRNmjRJly9f1tKlSxUWFsZKKQAAYDi7y02DBg3yfl21alXNmzfPoYEAAABKwu5zbho0aKBz587ZjF+8eDFf8QEAADCC3eXm2LFjys3NtRnPzs7WyZMnHRIKAACguIr8tdTq1avzfr127Vp5e3vnPc/NzVViYqLq16/v0HAAAAD2KnK56du3ryTJyclJMTEx+eYqV66s+vXrcydwAABguCKXG4vFIkkKCQnR9u3b5evrW2qhAAAAisvu1VJHjx4tjRwAAAAOUaRyM2vWrCLv8Lnnnit2GAAAgJIqUrmZMWNGkXbm5OREuQEAAIYqUrnhqygAAFBR2H2dm/9ktVpltVodlQUAAKDEilVuPvjgAzVv3lweHh7y8PBQixYt9OGHHzo6GwAAgN2KdePMl19+WSNGjFDHjh0lSUlJSXr66ad19uxZjR492uEhAQAAisrucvPWW29p7ty5GjRoUN7Ygw8+qKZNm+rvf/875QYAABjK7q+lUlNTFR0dbTMeHR2t1NRUh4QCAAAoLrvLTWhoqJYtW2YzvnTpUoWFhTkkFAAAQHHZ/bXUpEmT1L9/f23atCnvnJvNmzcrMTGxwNIDAABQlop85Gbv3r2SpIcfflg//vijfH19tWrVKq1atUq+vr7atm2b/vSnP5VaUAAAgKIo8pGbFi1aqG3btho+fLj+8pe/6KOPPirNXAAAAMVS5CM3GzduVNOmTfX8888rMDBQgwcP1vfff1+a2QAAAOxW5HJz9913a+HChUpNTdVbb72lo0ePqnPnzrrrrrs0ZcoUpaWllWZOAACAIrF7tVTVqlU1ZMgQbdy4UQcPHlS/fv00Z84c1a1bVw8++GBpZAQAACiyEt1bKjQ0VOPGjdP48ePl6empL7/80lG5AAAAisXupeA3bdq0SQsXLtSKFSvk7OysRx99VMOGDXNkNgAAALvZVW5OnTqlRYsWadGiRTp06JCio6M1a9YsPfroo6patWppZQQAACiyIpebnj176ttvv5Wvr68GDRqkoUOHqlGjRqWZDQAAwG5FPuemcuXKWr58uX7//XdNmTLFIcVm06ZN6tOnj4KCguTk5KRVq1bd9jUbNmxQ69at5ebmptDQUC1atKjEOQAAgHkUudysXr1aDz30kFxcXBz25llZWWrZsqXmzJlTpO2PHj2q3r17q2vXrkpOTtaoUaM0fPhwrV271mGZAABAxVbsE4odoWfPnurZs2eRt583b55CQkI0ffp0SVLjxo2VlJSkGTNmqEePHgW+Jjs7W9nZ2XnPMzMzSxYaAACUayVaCl7WtmzZou7du+cb69Gjh7Zs2VLoaxISEuTt7Z33CA4OLu2YAADAQBWq3KSlpcnf3z/fmL+/vzIzM3X16tUCXxMfH6+MjIy8x4kTJ8oiKgAAMIihX0uVBTc3N7m5uRkdAwAAlJEKdeQmICBA6enp+cbS09Pl5eUlDw8Pg1IBAIDypEKVm6ioKCUmJuYbW7dunaKiogxKBAAAyhtDy83ly5eVnJys5ORkSX8s9U5OTlZKSoqkP86XGTRoUN72Tz/9tI4cOaIxY8Zo//79+uc//6lly5Zp9OjRRsQHAADlkKHlZseOHYqIiFBERIQkKS4uThEREZowYYIkKTU1Na/oSFJISIi+/PJLrVu3Ti1bttT06dO1YMGCQpeBAwCAO4+hJxR36dJFVqu10PmCrj7cpUsX7d69uxRTAQCAiqxCnXMDAABwO5QbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKpQbAABgKuWi3MyZM0f169eXu7u72rdvr23bthW67aJFi+Tk5JTv4e7uXoZpAQBAeWZ4uVm6dKni4uI0ceJE7dq1Sy1btlSPHj10+vTpQl/j5eWl1NTUvMfx48fLMDEAACjPDC83b7zxhp544gkNGTJETZo00bx581SlShUtXLiw0Nc4OTkpICAg7+Hv71+GiQEAQHlmaLnJycnRzp071b1797wxZ2dnde/eXVu2bCn0dZcvX1a9evUUHByshx56SL/88kuh22ZnZyszMzPfAwAAmJeh5ebs2bPKzc21OfLi7++vtLS0Al/TqFEjLVy4UJ999pk++ugjWSwWRUdH6/fffy9w+4SEBHl7e+c9goODHf45AABA+WH411L2ioqK0qBBg9SqVSt17txZn376qWrVqqW33367wO3j4+OVkZGR9zhx4kQZJwYAAGWpkpFv7uvrKxcXF6Wnp+cbT09PV0BAQJH2UblyZUVEROjQoUMFzru5ucnNza3EWQEAQMVg6JEbV1dXtWnTRomJiXljFotFiYmJioqKKtI+cnNztWfPHgUGBpZWTAAAUIEYeuRGkuLi4hQTE6PIyEi1a9dOb775prKysjRkyBBJ0qBBg1S7dm0lJCRIkl555RV16NBBoaGhunjxoqZNm6bjx49r+PDhRn4M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PXuqWrVq8vf31+OPP66zZ88Wmi03N1fLly9Xnz598o3Xr19fkydP1tChQ+Xp6am6devmuzN1Tk6ORowYocDAQLm7u6tevXpKSEjIm69Ro4Y6duyoJUuWlPB3D4AjUG4AlAvvv/++fH19tW3bNj377LN65pln1K9fP0VHR2vXrl26//779fjjj+vKlSuSpIsXL6pbt26KiIjQjh079PXXXys9PV2PPvpooe/x888/KyMjQ5GRkTZz06dPV2RkpHbv3q2//vWveuaZZ/KOOM2aNUurV6/WsmXLdODAAX388ceqX79+vte3a9dO33//veN+QwAUG+UGQLnQsmVLjR8/XmFhYYqPj5e7u7t8fX31xBNPKCwsTBMmTNC5c+f0888/S/rjPJeIiAhNnjxZ4eHhioiI0MKFC7V+/XodPHiwwPc4fvy4XFxc5OfnZzPXq1cv/fWvf1VoaKjGjh0rX19frV+/XpKUkpKisLAwderUSfXq1VOnTp00YMCAfK8PCgrS8ePHHfy7AqA4KDcAyoUWLVrk/drFxUU1a9ZU8+bN88b8/f0lSadPn5b0x4nB69evzzuHp1q1agoPD5ckHT58uMD3uHr1qtzc3Ao86fk/3//mV2k332vw4MFKTk5Wo0aN9Nxzz+mbb76xeb2Hh0feUSUAxqpkdAAAkKTKlSvne+7k5JRv7GYhsVgskqTLly+rT58+mjJlis2+AgMDC3wPX19fXblyRTk5OXJ1db3t+998r9atW+vo0aP66quv9O233+rRRx9V9+7dtXz58rztz58/r1q1ahX14wIoRZQbABVS69attWLFCtWvX1+VKhXtr7JWrVpJkvbt25f366Ly8vJS//791b9/fz3yyCN64IEHdP78efn4+Ej64+TmiIgIu/YJoHTwtRSACik2Nlbnz5/XgAEDtH37dh0+fFhr167VkCFDlJubW+BratWqpdatWyspKcmu93rjjTf0r3/9S/v379fBgwf1ySefKCAgIN91er7//nvdf//9JflIAByEcgOgQgoKCtLmzZuVm5ur+++/X82bN9eoUaNUvXp1OTsX/lfb8OHD9fHHH9v1Xp6enpo6daoiIyPVtm1bHTt2TGvWrMl7ny1btigjI0OPPPJIiT4TAMfgIn4A7ihXr15Vo0aNtHTpUkVFRTlkn/3791fLli01btw4h+wPQMlw5AbAHcXDw0MffPDBLS/2Z4+cnBw1b95co0ePdsj+AJQcR24AAICpcOQGAACYCuUGAACYCuUGAACYCuUGAACYCuUGAACYCuUGAACYCuUGAACYCuUGAACYCuUGAACYyv8HMl5VsvU1YBcAAAAASUVORK5CYII=",
"text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -2543,22 +196,8 @@
}
],
"metadata": {
- "kernelspec": {
- "display_name": "Python [default]",
- "language": "python",
- "name": "python3"
- },
"language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.7.0"
+ "name": "python"
}
},
"nbformat": 4,
diff --git a/doc/source/examples/00TimeReversal.ipynb b/doc/source/examples/00TimeReversal.ipynb
new file mode 100644
index 000000000..1c277b060
--- /dev/null
+++ b/doc/source/examples/00TimeReversal.ipynb
@@ -0,0 +1,120 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "collapsed": false,
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
+ "source": [
+ "# Reversing a Pulse Template\n",
+ "\n",
+ "The `TimeReversalPulseTemplate` allows to reverse arbitrary pulse templates. Let us start with a pulse that has a clear time ordering."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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Glsp745hGP68SAACE6p73mr3JYEQAEnWW3aDAdJA44TinrfifUnXGOBgVl52g+U0gMCF+p634n1Ji1CQeL1enTx5MtLFQBDExcUpJsa8N20w2WXZDZbTAOonnPe8Ve5LAqFGMgxDBQUFOnr0aKSLgiBq3ry50tPTTX3zBosdlt1gOQ2g/sJ1z1vlvjR/CU3OFwS1adNGSUlJjviP084Mw5Db7daBAwckSW3bto1wiSLD7fGa+lscWaQBBAuBUCN4vV5/ENSyZctIFwdBkpiYKEk6cOCA2rRp45huslOZOdu0FQdjAjAvBks3gm9MUFJSUoRLgmDzfaZOGvdllWzTZJEGEEy0CAWB2b4xo/Gc+JlaMds0WaQBNBaBEAC/ytmmzZRksbokbWSRBoKjsfe61ZIonopACBV899136ty5s7Zt26Y+ffpEujh1Ov/889WnTx899NBDkS6KLZklySLjgoDQasy9bvX7kzFCcITi4mKlpqaqVatWKikpiXRxTM2MSRatmKQNMLtg3etWvz9pEYIjvPbaazrrrLNkGIZWrlyp0aNHR7pIpmX2JItWSdIGmF0o7nUr3p+0CAVReW6T0oj8GIZR73KWlZVpwYIF6tq1q+Lj49WhQwfdc889Fbb597//rQsuuEBJSUnq3bu3Nm7c6H/u0KFDGjNmjNq1a6ekpCT17NlTL730UoX9zz//fN1888267bbblJqaqvT0dP3pT3+qsE1UVJSefvpp/frXv1ZSUpIyMzP1xhtvVNjm888/19ChQ9WkSROlpaXpqquu0sGDB+v9Xn2eeeYZjR07VmPHjtUzzzwT8P5O40u4Vnm8UKDXWmP9556qOC4oyRVrmUoWMLPq7vXGsOL9SYtQEBWf9KrH7Hci8tpf3pVb7wyeM2fO1FNPPaWFCxdq8ODB2rdvn77++usK29x55526//77lZmZqTvvvFNjxozRzp07FRsbq59//ln9+vXTjBkz1KxZM61atUpXXXWVTj/9dPXv399/jOeee07Tpk3Tpk2btHHjRk2YMEHnnnuu/uu//su/zdy5c7VgwQLdd999evTRR3XllVcqPz9fqampOnr0qC688EJNnDhRCxcuVHFxsWbMmKHLL79c7777br3Pza5du7Rx40atWLFChmHolltuUX5+vjp27FjvYyD844WsPu4AgDXQIuQwx48f18MPP6wFCxZo/PjxOv300zV48GBNnDixwna33nqrhg8frjPOOENz585Vfn6+du7cKUlq166dbr31VvXp00ddunTRTTfdpCFDhmj58uUVjtGrVy/NmTNHmZmZGjdunLKzs5WXl1dhmwkTJmjMmDHq2rWr5s2bpxMnTmjz5s2SpMcee0xZWVmaN2+eunfvrqysLC1ZskRr167VN998U+/3vGTJEg0dOlQtWrRQamqqcnNz9eyzzzbk9DlOJMcLWX3cAQBroEUoiBLjYvTlXbkRe+36+Oqrr1RSUqKLLrqo1u169erl/923zMSBAwfUvXt3eb1ezZs3T8uXL9fevXvl8XhUUlJSJbHkqcfwHce3dEV12yQnJ6tZs2b+bf71r39p7dq1atKkSZXy7dq1S2eccUad79fr9eq5557Tww8/7H9s7NixuvXWWzV79mxFR/NdoDY1jSEI9RIclZfQsOK4A8CqArm/7bDcDYFQEFlh8Urf8hF1iYuL8//uuxnKysokSffdd58efvhhPfTQQ+rZs6eSk5M1depUeTyeGo/hO47vGPXZ5sSJE7rkkkt07733VilffdcAe+edd7R3794qg6O9Xq/y8vIqdNOhetVd16FcgqO6LjGrLN4I2EF972+7dF/zddhhMjMzlZiYWKWLKhAbNmzQiBEjNHbsWPXu3VtdunQJqKuqvvr27asvvvhCnTp1UteuXSv8JCcn1+sYzzzzjK644gpt3769ws8VV1zBoOkAhWsJDpbQAMKvIfe3Xe5VvmI5TEJCgmbMmKHbbrtNLpdL5557rn766Sd98cUXuvbaa+t1jMzMTL366qv68MMP1aJFCz344IPav3+/evToEdSy3njjjXrqqac0ZswY/+yznTt36uWXX9bTTz9d52KoP/30k95880298cYbOvvssys8N27cOP3617/W4cOHlZqaGtRy21V1S3AEM/N0dZlpWUIDCI/GLrFj5XuVQMiB/vjHPyo2NlazZ8/Wjz/+qLZt2+qGG26o9/6zZs3Sv//9b+Xm5iopKUnXXXedRo4cqWPHjgW1nBkZGdqwYYNmzJihiy++WCUlJerYsaOGDBlSr7E9zz//vJKTk6sdD3XRRRcpMTFRL7zwgm6++eagltvOKi/BEayZZDU1sbOEBhA+le/vQFj5XiUQcqDo6GjdeeeduvPOO6s816lTpyp5Ypo3b17hsdTUVK1cubLW11i3bl2VxyrvU10+mqNHj1b4OzMzUytWrAjodXymT5+u6dOnV/ucy+XSkSPW7teOFF8T+pZTgpYt+Ud0qMjToAHNhmHoUJGHGWKAydTU4mvldcWqY5lA6J577tGqVau0fft2uVyuKv9hVscwDM2ZM0dPPfWUjh49qnPPPVePP/64MjMzQ19gwKZqmknWkNah6lqCmCEGmEN197RdBkifyjKDpT0ej0aNGqXJkyfXe58FCxbokUce0eLFi7Vp0yYlJycrNzdXP//8cwhLCtifbyZZy2RXtXmGDhV5as1A7csYXbklKLtjC7VMdlkuMy1gF3XlDrNjfi/LtAjNnTtXkrR06dJ6bW8Yhh566CHNmjVLI0aMkFQ+ZiQtLU0rV67UFVdcEaqiAo5RW+tQj7bN/u9bZMV9DEMatXijvtxXWOFxKw+2BOyittxhp/4r2af11jKBUKB2796tgoIC5eTk+B9LSUnRgAEDtHHjxhoDoZKSkgqrkxcWFla7HYByvtahymOHvtxXqLPm1G/JGV9LkJUrU8AuasodVpld8ntZ/x3UoKCgQJKUlpZW4fG0tDT/c9WZP3++v/UJQP35vkm6Pd5qW3wqO7XFyOrfKAE7qm5ihI/Vu8NOFdFA6Pbbb682a/CpvvrqK3Xv3j1MJSpfkHTatGn+vwsLC9W+ffuwvT5gZVFRUUqOj9WqmwfXmYyN4Acwt1O7ySqz0/0b0UBo+vTpmjBhQq3bdOnSpUHHTk9PlyTt37+/wnIM+/fvV58+fWrcLz4+XvHx8Q16TQDlrLDcDIC6OeFejui7a926tVq3bh2SY3fu3Fnp6enKy8vzBz6FhYXatGlTQDPPAACAfVlm+vyePXu0fft27dmzR16v179m1IkTJ/zbdO/eXa+//rqk8ih26tSpuvvuu/XGG2/os88+07hx45SRkaGRI0dG6F0AAAAzsUx71+zZs/Xcc8/5/87KypIkrV27Vueff74kaceOHRWWebjttttUVFSk6667TkePHtXgwYP19ttvKyEhIaRl9Xg8Ki0tDelrnCo2NlYulytsrwcAgF1YJhBaunRpnTmEKidwi4qK0l133aW77rorhCWryOPxaPPmbSo6UVL3xkGS3CRe/ftnBRQMTZgwQc8995yuv/56LV68uMJzN954o/73f/9X48ePr3feJgAArMgygZBVlJaWquhEieLjW8vlCv2ga4+nREUnflJpaWnArULt27fXyy+/rIULFyoxMVGS9PPPP+vFF19Uhw4dQlFcAABMhUAoRFyueCUmJoXltUoa2PjUt29f7dq1SytWrNCVV14pSVqxYoU6dOigzp07B7GEAACYk2UGSyM0rrnmGj377LP+v5csWaKrr746giUCACB8CIQcbuzYsVq/fr3y8/OVn5+vDRs2aOzYsZEuFgAAYUHXmMO1bt1aw4cP19KlS2UYhoYPH65WrVpFulgAAIQFgRB0zTXXaMqUKZKkRYsWRbg0AACED4EQNGTIEHk8HkVFRSk3NzfSxQEAIGwIhELE4wlPHqFgvE5MTIy++uor/+8AADgFgVCQxcbGKrlJvIpO/NTgae2BSm4Sr9jYxn2UzZo1C1JpAACwDgKhIHO5XOrfP8v0S2zUlTF65cqVDS8QAAAWQSAUAi6Xi7W/AACwAPIIAQAAxyIQAgAAjkUgBAAAHItAKAgMw4h0ERBkfKYA4AwEQo0QFxcnSXK73REuCYLN95n6PmMAgD0xa6wRYmJi1Lx5cx04cECSlJSUpKioqAiXCo1hGIbcbrcOHDig5s2bk2ASAGyOQKiR0tPTJckfDMEemjdv7v9sAQD2RSDUSFFRUWrbtq3atGmjkydPRro4CIK4uDhaggDAIQiEgiQmJob/PAEAsBgGSwMAAMciEAIAAI5FIAQAAByLMUJ18CXWKywsjHBJADiJ2+1WUVGRSksPqajoeMhex+PxqKSkSIWFhSotLQ3Z6wDh5vt/u64EuQRCdTh+vLwCat++fYRLAgAAAnX8+HGlpKTU+HyUwVoCtSorK9OPP/6opk2b1jtZYmFhodq3b6/vv/9ezZo1C3EJzYlzwDnw4TxwDiTOgQ/nIXznwDAMHT9+XBkZGYqOrnkkEC1CdYiOjtZpp53WoH2bNWvm2Avdh3PAOfDhPHAOJM6BD+chPOegtpYgHwZLAwAAxyIQAgAAjkUgFALx8fGaM2eO4uPjI12UiOEccA58OA+cA4lz4MN5MN85YLA0AABwLFqEAACAYxEIAQAAxyIQAgAAjkUgBAAAHItAqIEWLVqkTp06KSEhQQMGDNDmzZtr3f6VV15R9+7dlZCQoJ49e+qtt94KU0mDb/78+TrnnHPUtGlTtWnTRiNHjtSOHTtq3Wfp0qWKioqq8JOQkBCmEofGn/70pyrvqXv37rXuY6frQJI6depU5RxERUXpxhtvrHZ7O1wH77//vi655BJlZGQoKipKK1eurPC8YRiaPXu22rZtq8TEROXk5Ojbb7+t87iB1imRVNs5OHnypGbMmKGePXsqOTlZGRkZGjdunH788cdaj9mQ+ynS6roWJkyYUOU9DRkypM7j2uVakFRt/RAVFaX77ruvxmOG+1ogEGqAZcuWadq0aZozZ462bt2q3r17Kzc3VwcOHKh2+w8//FBjxozRtddeq23btmnkyJEaOXKkPv/88zCXPDjee+893Xjjjfroo4+0evVqnTx5UhdffLGKiopq3a9Zs2bat2+f/yc/Pz9MJQ6ds846q8J7Wr9+fY3b2u06kKSPP/64wvtfvXq1JGnUqFE17mP166CoqEi9e/fWokWLqn1+wYIFeuSRR7R48WJt2rRJycnJys3N1c8//1zjMQOtUyKttnPgdru1detW/fGPf9TWrVu1YsUK7dixQ5deemmdxw3kfjKDuq4FSRoyZEiF9/TSSy/Vekw7XQuSKrz3ffv2acmSJYqKitJll11W63HDei0YCFj//v2NG2+80f+31+s1MjIyjPnz51e7/eWXX24MHz68wmMDBgwwrr/++pCWM1wOHDhgSDLee++9Grd59tlnjZSUlPAVKgzmzJlj9O7du97b2/06MAzD+MMf/mCcfvrpRllZWbXP2+06kGS8/vrr/r/LysqM9PR047777vM/dvToUSM+Pt546aWXajxOoHWKmVQ+B9XZvHmzIcnIz8+vcZtA7yezqe48jB8/3hgxYkRAx7H7tTBixAjjwgsvrHWbcF8LtAgFyOPx6JNPPlFOTo7/sejoaOXk5Gjjxo3V7rNx48YK20tSbm5ujdtbzbFjxyRJqamptW534sQJdezYUe3bt9eIESP0xRdfhKN4IfXtt98qIyNDXbp00ZVXXqk9e/bUuK3drwOPx6MXXnhB11xzTa0LFNvxOvDZvXu3CgoKKnzOKSkpGjBgQI2fc0PqFKs5duyYoqKi1Lx581q3C+R+sop169apTZs26tatmyZPnqxDhw7VuK3dr4X9+/dr1apVuvbaa+vcNpzXAoFQgA4ePCiv16u0tLQKj6elpamgoKDafQoKCgLa3krKyso0depUnXvuuTr77LNr3K5bt25asmSJ/va3v+mFF15QWVmZBg0apB9++CGMpQ2uAQMGaOnSpXr77bf1+OOPa/fu3frlL3+p48ePV7u9na8DSVq5cqWOHj2qCRMm1LiNHa+DU/k+y0A+54bUKVby888/a8aMGRozZkytC2wGej9ZwZAhQ/T8888rLy9P9957r9577z0NHTpUXq+32u3tfi0899xzatq0qX7zm9/Uul24rwVWn0ej3Hjjjfr888/r7L8dOHCgBg4c6P970KBBOvPMM/XEE0/oz3/+c6iLGRJDhw71/96rVy8NGDBAHTt21PLly+v1jcdunnnmGQ0dOlQZGRk1bmPH6wA1O3nypC6//HIZhqHHH3+81m3teD9dccUV/t979uypXr166fTTT9e6det00UUXRbBkkbFkyRJdeeWVdU6QCPe1QItQgFq1aqWYmBjt37+/wuP79+9Xenp6tfukp6cHtL1VTJkyRX//+9+1du1anXbaaQHtGxcXp6ysLO3cuTNEpQu/5s2b64wzzqjxPdn1OpCk/Px8rVmzRhMnTgxoP7tdB77PMpDPuSF1ihX4gqD8/HytXr261tag6tR1P1lRly5d1KpVqxrfk12vBUn64IMPtGPHjoDrCCn01wKBUIBcLpf69eunvLw8/2NlZWXKy8ur8E33VAMHDqywvSStXr26xu3NzjAMTZkyRa+//rreffddde7cOeBjeL1effbZZ2rbtm0IShgZJ06c0K5du2p8T3a7Dk717LPPqk2bNho+fHhA+9ntOujcubPS09MrfM6FhYXatGlTjZ9zQ+oUs/MFQd9++63WrFmjli1bBnyMuu4nK/rhhx906NChGt+THa8Fn2eeeUb9+vVT7969A9435NdC2IZl28jLL79sxMfHG0uXLjW+/PJL47rrrjOaN29uFBQUGIZhGFdddZVx++23+7ffsGGDERsba9x///3GV199ZcyZM8eIi4szPvvss0i9hUaZPHmykZKSYqxbt87Yt2+f/8ftdvu3qXwO5s6da7zzzjvGrl27jE8++cS44oorjISEBOOLL76IxFsIiunTpxvr1q0zdu/ebWzYsMHIyckxWrVqZRw4cMAwDPtfBz5er9fo0KGDMWPGjCrP2fE6OH78uLFt2zZj27ZthiTjwQcfNLZt2+afEfU///M/RvPmzY2//e1vxqeffmqMGDHC6Ny5s1FcXOw/xoUXXmg8+uij/r/rqlPMprZz4PF4jEsvvdQ47bTTjO3bt1eoI0pKSvzHqHwO6rqfzKi283D8+HHj1ltvNTZu3Gjs3r3bWLNmjdG3b18jMzPT+Pnnn/3HsPO14HPs2DEjKSnJePzxx6s9RqSvBQKhBnr00UeNDh06GC6Xy+jfv7/x0Ucf+Z8777zzjPHjx1fYfvny5cYZZ5xhuFwu46yzzjJWrVoV5hIHj6Rqf5599ln/NpXPwdSpU/3nKy0tzRg2bJixdevW8Bc+iEaPHm20bdvWcLlcRrt27YzRo0cbO3fu9D9v9+vA55133jEkGTt27KjynB2vg7Vr11Z7/fveZ1lZmfHHP/7RSEtLM+Lj442LLrqoyrnp2LGjMWfOnAqP1VanmE1t52D37t011hFr1671H6PyOajrfjKj2s6D2+02Lr74YqN169ZGXFyc0bFjR2PSpElVAho7Xws+TzzxhJGYmGgcPXq02mNE+lqIMgzDCE1bEwAAgLkxRggAADgWgRAAAHAsAiEAAOBYBEIAAMCxCIQAAIBjEQgBAADHIhACAACORSAEAAAci0AIgKlNmDBBI0eOjNjrX3XVVZo3b15QjuXxeNSpUydt2bIlKMcD0HhklgYQMVFRUbU+P2fOHN1yyy0yDEPNmzcPT6FO8a9//UsXXnih8vPz1aRJk6Ac87HHHtPrr79eZQFeAJFBIAQgYgoKCvy/L1u2TLNnz9aOHTv8jzVp0iRoAUhDTJw4UbGxsVq8eHHQjnnkyBGlp6dr69atOuuss4J2XAANQ9cYgIhJT0/3/6SkpCgqKqrCY02aNKnSNXb++efrpptu0tSpU9WiRQulpaXpqaeeUlFRka6++mo1bdpUXbt21T/+8Y8Kr/X5559r6NChatKkidLS0nTVVVfp4MGDNZbN6/Xq1Vdf1SWXXFLh8U6dOmnevHm65ppr1LRpU3Xo0EFPPvmk/3mPx6MpU6aobdu2SkhIUMeOHTV//nz/8y1atNC5556rl19+uZFnD0AwEAgBsJznnntOrVq10ubNm3XTTTdp8uTJGjVqlAYNGqStW7fq4osv1lVXXSW32y1JOnr0qC688EJlZWVpy5Ytevvtt7V//35dfvnlNb7Gp59+qmPHjik7O7vKcw888ICys7O1bds2/f73v9fkyZP9LVmPPPKI3njjDS1fvlw7duzQX//6V3Xq1KnC/v3799cHH3wQvBMCoMEIhABYTu/evTVr1ixlZmZq5syZSkhIUKtWrTRp0iRlZmZq9uzZOnTokD799FNJ5eNysrKyNG/ePHXv3l1ZWVlasmSJ1q5dq2+++aba18jPz1dMTIzatGlT5blhw4bp97//vbp27aoZM2aoVatWWrt2rSRpz549yszM1ODBg9WxY0cNHjxYY8aMqbB/RkaG8vPzg3xWADQEgRAAy+nVq5f/95iYGLVs2VI9e/b0P5aWliZJOnDggKTyQc9r1671jzlq0qSJunfvLknatWtXta9RXFys+Pj4agd0n/r6vu4832tNmDBB27dvV7du3XTzzTfrn//8Z5X9ExMT/a1VACIrNtIFAIBAxcXFVfg7KiqqwmO+4KWsrEySdOLECV1yySW69957qxyrbdu21b5Gq1at5Ha75fF45HK56nx932v17dtXu3fv1j/+8Q+tWbNGl19+uXJycvTqq6/6tz98+LBat25d37cLIIQIhADYXt++ffXaa6+pU6dOio2tX7XXp08fSdKXX37p/72+mjVrptGjR2v06NH67W9/qyFDhujw4cNKTU2VVD5wOysrK6BjAggNusYA2N6NN96ow4cPa8yYMfr444+1a9cuvfPOO7r66qvl9Xqr3ad169bq27ev1q9fH9BrPfjgg3rppZf09ddf65tvvtErr7yi9PT0CnmQPvjgA1188cWNeUsAgoRACIDtZWRkaMOGDfJ6vbr44ovVs2dPTZ06Vc2bN1d0dM3V4MSJE/XXv/41oNdq2rSpFixYoOzsbJ1zzjn67rvv9NZbb/lfZ+PGjTp27Jh++9vfNuo9AQgOEioCQA2Ki4vVrVs3LVu2TAMHDgzKMUePHq3evXvrjjvuCMrxADQOLUIAUIPExEQ9//zztSZeDITH41HPnj11yy23BOV4ABqPFiEAAOBYtAgBAADHIhACAACORSAEAAAci0AIAAA4FoEQAABwLAIhAADgWARCAADAsQiEAACAYxEIAQAAx/r/xY6qCvTNzF0AAAAASUVORK5CYII=",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from qupulse.pulses import TimeReversalPT, FunctionPT, TablePT, plotting\n",
+ "\n",
+ "forward_1 = FunctionPT('sin(2*pi*t / 10)', duration_expression='10', channel='A')\n",
+ "wait = TablePT({'A': [(0, 0), (3, 0)]}, measurements=[('M', 0.5, 1)])\n",
+ "forward_2 = FunctionPT('sin(2*pi*t / 5)', duration_expression='5', channel='A')\n",
+ "\n",
+ "forward_all = forward_1 @ wait @ forward_2\n",
+ "\n",
+ "_ = plotting.plot(forward_all, plot_measurements={'M'}, show=False)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "collapsed": false,
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
+ "source": [
+ "We can now easily create the same pulse backward"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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brrpprAUDPZRkf3VeZlf3R4vw3mBCHBqneI00apD65VHaOjQoXK73SouLlazZs104MABpaamqlWrVgRCqMTJ2V9z714T1eyogBM4uU5xmmjVoSE/Grv55pt1ySWX6PDhw0pJSdEHH3yggoIC9enTR/fff38kyggLq5r91e6ZX30ZUn182VEBhIfT6hSniUUdGnKL0Pbt2/XEE08oPj5eCQkJKi0tVadOnTR//nyNGzdOv/nNbyJRTtjAlpl5ap7msnXriC9D6sFiT9SzowJO44Q6xWliUYeG3CKUmJio+PiKzVq1aqU9e/ZIktLT0xk+j1qlupzRX6YiQyrfUIFIc0qd4jTRrkNDbhHKycnRhx9+qOzsbJ133nmaNWuWDhw4oL/85S86++yzI1FGAACAiAi5RWju3Llq3bq1JOmee+5R06ZNNWnSJP3444968sknw15AAACASAm5RSg3N9f//1atWumtt94Ka4EAAACihYSKAADAsYIKhAYPHqwPPvigzvWOHTume++9VwsXLjzlggEA4FORRLGMRIoO5PZ45faUyTCMiOw/qEdjI0eO1GWXXab09HRdcsklys3NVVZWlpKTk3X48GF9/vnnWr9+vd58800NGzZM9913X0QKCwBwHpIoOptvGH2kkisGFQhde+21GjNmjF555RUtXbpUTz75pI4ePSqpYphbt27dNGjQIH344Yc688wzw1pAAICzVU2iKJFI0e58iRW3nPS5+5IrprrCNl+8pBA6SyclJWnMmDEaM2aMJOno0aMqKSlR8+bNlZiYGNZCAQBQky0z85TqSmAeP5vzJVYsOeGV2+ONaHLFeodV6enpSk9PD2dZAACoVaorIewtAjCnisSKkf+sGTUGAAAci0AIAAA4FoEQAABwLMsFQgsXLlSHDh2UnJysfv36afPmzbWu/8orr6hr165KTk5W9+7d9eabb0appAAAwOzqFQgdOXJETz/9tGbMmKFDhw5JkrZu3arvv/8+rIWraunSpZo6dapmz56trVu3qmfPnho0aJD2799f4/rvv/++Ro8erWuvvVbbtm3TiBEjNGLECH366acRLScAALCGkLtjf/zxx8rLy1N6erq++eYbTZw4Uc2aNdPy5cu1Z88ePf/885EopyTpwQcf1MSJE3X11VdLkhYtWqSVK1dq8eLFuu2226qt//DDD2vw4MH6f//v/0mS/vznP2v16tV67LHHtGjRooiU0TAMlZyonPnUicM8feeBLLAy7Tlwe7wq9RpKLCtXXFnkyvhTWblKvUbFeWhQFvL2Trx/UIF6BNEQciA0depUjR8/XvPnz1ejRo38y4cOHarf/e53YS3cyTwejz766CPNmDHDvyw+Pl55eXnauHFjjdts3LhRU6dOrbRs0KBBWrFiRcDjlJaWqrS01P97UVFRSOUsOeFVt1mrKi2LVDZMsyILbGWRzH8RHjuic5j33q3XZk67f1CBegTREvKjsQ8//FDXX399teVt2rRRYWFhWApVkwMHDsjr9SojI6PS8oyMjIDHLSwsDGl9SZo3b54/R1J6erratm17ymX3ZcN0CrLA/pwVFafOafcPKlCPIFpCbhFKSkqqsZXkq6++UsuWLcNSqFiaMWNGpVakoqKikIKhlMQEfX7XIEmKeDZMK3BqFtiTs6Kaldtdovc3bFXDhm2UkpISseOUlJTo+PHvNeDc3kpNDf443D/wcWo9ggon/12NRCAcciB06aWX6q677tKyZcskVVT4e/bs0fTp03XZZZeFvYA+LVq0UEJCgvbt21dp+b59+5SZmVnjNpmZmSGtL1UEeklJSfUuZ7QyYVqFk7PAmv5aKEtQUkKckhvEK7lB5L5lGw3idSIhztHXAk4N146zRbouDfnR2AMPPKDjx4+rVatWKikp0XnnnafOnTurUaNGuueeeyJRRkmSy+VSnz59lJ+f719WXl6u/Px89e/fv8Zt+vfvX2l9SVq9enXA9QEAgLOEHGKlp6dr9erVWr9+vT7++GMdP35cvXv3Vl5eXiTKV8nUqVM1btw45ebmqm/fvnrooYdUXFzsH0U2duxYtWnTRvPmzZMk/fGPf9R5552nBx54QMOGDdPLL7+sLVu26Mknn4x4WQEAgPnVu61p4MCBGjhwYDjLUqdRo0bpxx9/1KxZs1RYWKhevXrprbfe8neI3rNnj+Ljf27kGjBggF588UXNnDlTt99+u7Kzs7VixQqdffbZUS03AAAwp5ADoUceeaTG5XFxcUpOTlbnzp31q1/9SgkJkelzMHnyZE2ePLnG19atW1dt2ciRIzVy5MiIlAUAAFhbyIHQggUL9OOPP8rtdqtp04rhwYcPH1ZqaqoaNmyo/fv3q1OnTlq7dm1Yhp4DAABESsidpefOnatzzjlHX3/9tQ4ePKiDBw/qq6++Ur9+/fTwww9rz549yszM1M033xyJ8gJwILfHK7enTIZhxLooiALDMMgmjagJuUVo5syZeu2113T66af7l3Xu3Fn333+/LrvsMv3nP//R/PnzIzqUHoCz+PIJkWXa/sgojWgLuUVo7969KiurPl9QWVmZP2NzVlaWjh07duqlA+BYNWXnJsu0/VXNKE02aURayC1CF1xwga6//no9/fTTysnJkSRt27ZNkyZN0oUXXihJ+uSTT9SxY8fwlhSAo5ycnZss0860ZWaemqe5aAFERIXcIvTMM8+oWbNm6tOnjz8Lc25urpo1a6ZnnnlGktSwYUM98MADYS8sAGfxZZRNddEi4ESpLqbUQOSF3CKUmZmp1atX68svv9RXX30lSerSpYu6dOniX+eCCy4IXwkBAAAipN4JFbt27aquXbuGsywAAABRVa9A6LvvvtMbb7yhPXv2yOPxVHrtwQcfDEvBAAAAIi3kQCg/P1+XXnqpOnXqpC+//FJnn322vvnmGxmGod69e0eijAAAABERcmfpGTNm6JZbbtEnn3yi5ORkvfbaa/r222913nnnMZVFLdweryOSwZEIDZFGckV7qqg7yqg/EHUhtwh98cUXeumllyo2btBAJSUlatiwoe666y4NHz5ckyZNCnsh7SD37jW2TwZHIjREA8kV7Ye6A7EUcotQWlqav19Q69attWvXLv9rBw4cCF/JbKBqQji7J4MjERoiheSK9la17pCoPxA9IbcI/eIXv9D69et15plnaujQoZo2bZo++eQTLV++XL/4xS8iUUbL8iWEO1jscVwyOBKhIZxIrugcW2bmKdWVoJREcgghOkIOhB588EEdP35ckjRnzhwdP35cS5cuVXZ2NiPGalCREM5532pIhIZw8yVXhL2luhL4nBFVIV9tnTp18v8/LS1NixYtCmuBAAAAoiXkPkKdOnXSwYMHqy0/cuRIpSAJAADA7EIOhL755ht5vdU7KJaWlur7778PS6EAAACiIehHY2+88Yb//6tWrVJ6err/d6/Xq/z8fHXo0CGshQMAAIikoAOhESNGSKrosDhu3LhKryUmJqpDhw7MOA8AACwl6ECovLxcktSxY0d9+OGHatGiRcQKBQDBcHu8DLO2MMMw/CkRgFgJedTY7t27I1EOAAiZEzK22xXZpGEWQQVCjzzy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vSasnTPpLAmcunDfl5HGfQ/8jBYhC6jaQTOcnTPpLAnUTyTvy0hjOg3gZ7QIWYCvg+bBYk9EO2fSWRIIXrTuy0hz2nQaXq9XJ06ciHUxEAaJiYlKSDj1AJ5AyCIqOmhG9hsbnSWB0ETjvow0p0ynYRiGCgsLdeTIkVgXBWHUpEkTZWZmntI1zF89AAgTs2ebdnIWaV8Q1KpVK6Wmppry80HwDMOQ2+3W/v37JUmtW7eu974IhAAgTMycbdrJHaS9Xq8/CGrevHmsi4MwSUlJkSTt379frVq1qvdjMjpLA8ApsEq2aScPjPD1CUpNTY1xSRBuvs/0VPp90SIEAKfAitmmnTowwknv1SnC8ZkSCFlUOPoikFANCI+ask27PV5TBBo19QtiYATwM+4EizrVvghO7i8ARIMZkixyn9vbN998o44dO2rbtm3q1atXrItTp/PPP1+9evXSQw89FOuiVEIfIQsJZ18EEqoB4We2JItO7hcE6yopKVGzZs3UokULlZaWRvx4tAhZSKT6IjgtoRoQKWZOsujUfkGwntdee01nnXWWDMPQihUrNGrUqIgejxYhi/H1RQhnEjenJFQDoqFqkkW3xyu3pyzqk7JW7QPo6xfEvV7h58mmo/8TyrVQXl6u+fPnq3PnzkpKSlK7du10zz33VFrnP//5jy644AKlpqaqZ8+e2rhxo/+1gwcPavTo0WrTpo1SU1PVvXt3vfTSS5W2P//883XTTTfp1ltvVbNmzZSZmak//elPldaJi4vT008/rV//+tdKTU1Vdna23njjjUrrfPrppxoyZIgaNmyojIwMXXXVVTpw4EDQ79XnmWee0ZgxYzRmzBg988wzIW8fKlqEACCCYpFbiL5BdSs54VW3WaticuzP7xoUdGf1GTNm6KmnntKCBQs0cOBA7d27V19++WWlde644w7df//9ys7O1h133KHRo0dr586datCggX766Sf16dNH06dPV+PGjbVy5UpdddVVOv3009W3b1//Pp577jlNnTpVmzZt0saNGzV+/Hide+65+q//+i//OnPmzNH8+fN133336dFHH9WVV16pgoICNWvWTEeOHNGFF16oCRMmaMGCBSopKdH06dN1+eWX6+233w763OzatUsbN27U8uXLZRiGbr75ZhUUFKh9+/ZB7yNUtAgBQJjFOrcQfQDt4dixY3r44Yc1f/58jRs3TqeffroGDhyoCRMmVFrvlltu0bBhw3TGGWdozpw5Kigo0M6dOyVJbdq00S233KJevXqpU6dO+sMf/qDBgwdr2bJllfbRo0cPzZ49W9nZ2Ro7dqxyc3OVn59faZ3x48dr9OjR6ty5s+bOnavjx49r8+bNkqTHHntMOTk5mjt3rrp27aqcnBwtXrxYa9eu1VdffRX0e168eLGGDBmipk2bqlmzZho0aJCeffbZ+py+oNEiZAOhDKV3cop9IFoC9eeL9BQcNd3f9AGsWUpigj6/a1DMjh2ML774QqWlpbroootqXa9Hjx7+//ummti/f7+6du0qr9eruXPnatmyZfr+++/l8XhUWlpaLbnkyfvw7cc3fUVN66Slpalx48b+df79739r7dq1atiwYbXy7dq1S2eccUad79fr9eq5557Tww8/7F82ZswY3XLLLZo1a5bi4yPTdkMgZAPBNr3TXA5ET025hSL5mCzQ/U0fwJrV9PmYjW8KibokJib6/+/7rMvLyyVJ9913nx5++GE99NBD6t69u9LS0jRlyhR5PJ6A+/Dtx7ePYNY5fvy4LrnkEt17773VyhfsPGCrVq3S999/X61ztNfrVX5+fqXHdOHEozGLqk/TO0NpgeiL1mMy7m/7yc7OVkpKSrVHVKHYsGGDhg8frjFjxqhnz57q1KlTSI+qgtW7d2999tln6tChgzp37lzpJy0tLah9PPPMM7riiiu0ffv2Sj9XXHFFRDtNmzscRkCnOpSeobRAdNT2mCxc91/VEWLc3/aQnJys6dOn69Zbb5XL5dK5556rH3/8UZ999pmuvfbaoPaRnZ2tV199Ve+//76aNm2qBx98UPv27VO3bt3CWtYbb7xRTz31lEaPHu0ffbZz5069/PLLevrpp+ucEPXHH3/U3//+d73xxhs6++yzK702duxY/frXv9ahQ4fUrFmzsJZbokXI0k5lKD1DaYHoqelezb17jUYu2njKw+p9j8RO/jLE/W0fd955p6ZNm6ZZs2bpzDPP1KhRo6r13anNzJkz1bt3bw0aNEjnn3++MjMzNWLEiLCXMysrSxs2bJDX69XFF1+s7t27a8qUKWrSpElQfXuef/55paWl1dgf6qKLLlJKSopeeOGFsJdbokXIdgJ9y2ReMSD2fI/JtvzfI6wtBYd1sNhTr9abkztGM0LMvuLj43XHHXfojjvuqPZahw4dqgXSTZo0qbSsWbNmWrFiRa3HWLduXbVlVbepKWA/cuRIpd+zs7O1fPnykI7jM23aNE2bNq3G11wulw4fjlzfVssEQvfcc49Wrlyp7du3y+VyVfsAamIYhmbPnq2nnnpKR44c0bnnnqvHH39c2dnZkS9wjNQ0vxGdpAFzqCnzdH06UAe6pxkhBoTOMo/GPB6PRo4cqUmTJgW9zfz58/XII49o0aJF2rRpk9LS0jRo0CD99NNPESxp9NU1vxE5RQDziIuLU/M0V40dqA8We2p9VObLhnyw2FNjx2iCICB0lmkRmjNnjiRpyZIlQa1vGIYeeughzZw5U8OHD5dU8QwyIyNDK1as0BVXXBGpokZdTd8yT34MRk4RwFwCdaDOvXuNurVu/H8tQ5W3MQxp5KKN+nxvUaXldIwGTo1lAqFQ7d69W4WFhcrLy/MvS09PV79+/bRx48aAgVBpaWml2W6LiopqXM9sqs5vFGgUGTlFAHPwdaCu2m/o871FOmt2cFM/0AoEnDrbBkKFhYWSpIyMjErLMzIy/K/VZN68ef7WJ6upWqFWxSMxwHx8rUNuj7fGFp+qTm4xohUIOHUxDYRuu+22GrNQnuyLL75Q165do1Siignupk6d6v+9qKhIbdu2jdrxT8XJze01odIEzCkuLk5pSQ208qaBdSZa5D4GwiumgdC0adM0fvz4Wtfp1KlTvfadmZkpSdq3b1+l9N779u1Tr169Am6XlJSkpKSkeh3TDKyQNh5Azbh/geiL6R3XsmVLtWzZMiL77tixozIzM5Wfn+8PfIqKirRp06aQRp4BAAD7sszw+T179mj79u3as2ePvF6vfw6S48eP+9fp2rWrXn/9dUkV36ymTJmiu+++W2+88YY++eQTjR07VllZWRHJqgkAAKzHMm2ws2bN0nPPPef/PScnR5K0du1anX/++ZKkHTt26OjRo/51br31VhUXF+u6667TkSNHNHDgQL311ltKTk6OatkBAObk8XhUVlYWteM1aNBALpcrasdD3SwTCC1ZsqTOHEJVE5HFxcXprrvu0l133RXBkgEArMjj8Wjz5m0qPl5a98phktYwSX375gQdDI0fP17PPfecrr/+ei1atKjSazfeeKP+93//V+PGjQs6xx6qs0wgBABAOJWVlan4eKmSklrK5Yr8IBmPp1TFx39UWVlZSK1Cbdu21csvv6wFCxYoJSVFkvTTTz/pxRdfVLt27SJVXMcgEAIAOJrLlaSUlNSoHKu0Ho1PvXv31q5du7R8+XJdeeWVkqTly5erXbt26tixY5hL6DyW6SwNAIBTXXPNNXr22Wf9vy9evFhXX311DEtkHwRCAACY3JgxY7R+/XoVFBSooKBAGzZs0JgxY2JdLFvg0RgAACbXsmVLDRs2TEuWLJFhGBo2bJhatGgR62LZAoEQAAAWcM0112jy5MmSpIULF8a4NPZBIAQAgAUMHjxYHo9HcXFxGjRoUKyLYxsEQgAAR/N4opNH6FSPk5CQoC+++ML/f4QHgRAAwJEaNGigtIZJKj7+Y72GtddHWsMkNWhQ/z+9jRs3DmNpIBEIAQAcyuVyqW/fHFNPsVFXxugVK1acWoFAIAQAcC6Xy8XcXw5HHiEAAOBYBEIAAMCxCIQAAIBjEQgBABzBMIxYFwFhFo7PlEAIAGBriYmJkiS32x3jkiDcfJ+p7zOuD0aNAQBsLSEhQU2aNNH+/fslSampqYqLi4txqXAqDMOQ2+3W/v371aRJk1NKMEkgBACwvczMTEnyB0OwhyZNmvg/2/oiEAIA2F5cXJxat26tVq1a6cSJE7EuDsIgMTExLFONEAgBABwjISGBebpQCZ2lAQCAYxEIAQAAxyIQAgAAjkUfoTr4kjUVFRXFuCRAeLndbhUXF6us7KCKi49F7Dgej0elpcUqKiqK6izfAJzN93e7rqSLBEJ1OHas4g9E27ZtY1wSAAAQqmPHjik9PT3g63EGOcdrVV5erh9++EGNGjUKOgFXUVGR2rZtq2+//VaNGzeOcAnNiXPAOfDhPHAOJM6BD+cheufAMAwdO3ZMWVlZio8P3BOIFqE6xMfH67TTTqvXto0bN3bshe7DOeAc+HAeOAcS58CH8xCdc1BbS5APnaUBAIBjEQgBAADHIhCKgKSkJM2ePVtJSUmxLkrMcA44Bz6cB86BxDnw4TyY7xzQWRoAADgWLUIAAMCxCIQAAIBjEQgBAADHIhACAACORSBUTwsXLlSHDh2UnJysfv36afPmzbWu/8orr6hr165KTk5W9+7d9eabb0appOE3b948nXPOOWrUqJFatWqlESNGaMeOHbVus2TJEsXFxVX6SU5OjlKJI+NPf/pTtffUtWvXWrex03UgSR06dKh2DuLi4nTjjTfWuL4droN3331Xl1xyibKyshQXF6cVK1ZUet0wDM2aNUutW7dWSkqK8vLy9PXXX9e531DrlFiq7RycOHFC06dPV/fu3ZWWlqasrCyNHTtWP/zwQ637rM/9FGt1XQvjx4+v9p4GDx5c537tci1IqrF+iIuL03333Rdwn9G+FgiE6mHp0qWaOnWqZs+era1bt6pnz54aNGiQ9u/fX+P677//vkaPHq1rr71W27Zt04gRIzRixAh9+umnUS55eLzzzju68cYb9cEHH2j16tU6ceKELr74YhUXF9e6XePGjbV3717/T0FBQZRKHDlnnXVWpfe0fv36gOva7TqQpA8//LDS+1+9erUkaeTIkQG3sfp1UFxcrJ49e2rhwoU1vj5//nw98sgjWrRokTZt2qS0tDQNGjRIP/30U8B9hlqnxFpt58Dtdmvr1q268847tXXrVi1fvlw7duzQpZdeWud+Q7mfzKCua0GSBg8eXOk9vfTSS7Xu007XgqRK733v3r1avHix4uLidNlll9W636heCwZC1rdvX+PGG2/0/+71eo2srCxj3rx5Na5/+eWXG8OGDau0rF+/fsb1118f0XJGy/79+w1JxjvvvBNwnWeffdZIT0+PXqGiYPbs2UbPnj2DXt/u14FhGMYf//hH4/TTTzfKy8trfN1u14Ek4/XXX/f/Xl5ebmRmZhr33Xeff9mRI0eMpKQk46WXXgq4n1DrFDOpeg5qsnnzZkOSUVBQEHCdUO8ns6npPIwbN84YPnx4SPux+7UwfPhw48ILL6x1nWhfC7QIhcjj8eijjz5SXl6ef1l8fLzy8vK0cePGGrfZuHFjpfUladCgQQHXt5qjR49Kkpo1a1bresePH1f79u3Vtm1bDR8+XJ999lk0ihdRX3/9tbKystSpUyddeeWV2rNnT8B17X4deDwevfDCC7rmmmtqnaDYjteBz+7du1VYWFjpc05PT1e/fv0Cfs71qVOs5ujRo4qLi1OTJk1qXS+U+8kq1q1bp1atWqlLly6aNGmSDh48GHBdu18L+/bt08qVK3XttdfWuW40rwUCoRAdOHBAXq9XGRkZlZZnZGSosLCwxm0KCwtDWt9KysvLNWXKFJ177rk6++yzA67XpUsXLV68WH/729/0wgsvqLy8XAMGDNB3330XxdKGV79+/bRkyRK99dZbevzxx7V792798pe/1LFjx2pc387XgSStWLFCR44c0fjx4wOuY8fr4GS+zzKUz7k+dYqV/PTTT5o+fbpGjx5d6wSbod5PVjB48GA9//zzys/P17333qt33nlHQ4YMkdfrrXF9u18Lzz33nBo1aqTf/OY3ta4X7WuB2edxSm688UZ9+umndT6/7d+/v/r37+//fcCAATrzzDP1xBNP6M9//nOkixkRQ4YM8f+/R48e6tevn9q3b69ly5YF9Y3Hbp555hkNGTJEWVlZAdex43WAwE6cOKHLL79chmHo8ccfr3VdO95PV1xxhf//3bt3V48ePXT66adr3bp1uuiii2JYsthYvHixrrzyyjoHSET7WqBFKEQtWrRQQkKC9u3bV2n5vn37lJmZWeM2mZmZIa1vFZMnT9Y//vEPrV27VqeddlpI2yYmJionJ0c7d+6MUOmir0mTJjrjjDMCvie7XgeSVFBQoDVr1mjChAkhbWe368D3WYbyOdenTrECXxBUUFCg1atX19oaVJO67icr6tSpk1q0aBHwPdn1WpCk9957Tzt27Ai5jpAify0QCIXI5XKpT58+ys/P9y8rLy9Xfn5+pW+6J+vfv3+l9SVp9erVAdc3O8MwNHnyZL3++ut6++231bFjx5D34fV69cknn6h169YRKGFsHD9+XLt27Qr4nux2HZzs2WefVatWrTRs2LCQtrPbddCxY0dlZmZW+pyLioq0adOmgJ9zfeoUs/MFQV9//bXWrFmj5s2bh7yPuu4nK/ruu+908ODBgO/JjteCzzPPPKM+ffqoZ8+eIW8b8Wshat2ybeTll182kpKSjCVLlhiff/65cd111xlNmjQxCgsLDcMwjKuuusq47bbb/Otv2LDBaNCggXH//fcbX3zxhTF79mwjMTHR+OSTT2L1Fk7JpEmTjPT0dGPdunXG3r17/T9ut9u/TtVzMGfOHGPVqlXGrl27jI8++si44oorjOTkZOOzzz6LxVsIi2nTphnr1q0zdu/ebWzYsMHIy8szWrRoYezfv98wDPtfBz5er9do166dMX369Gqv2fE6OHbsmLFt2zZj27ZthiTjwQcfNLZt2+YfEfU///M/RpMmTYy//e1vxscff2wMHz7c6Nixo1FSUuLfx4UXXmg8+uij/t/rqlPMprZz4PF4jEsvvdQ47bTTjO3bt1eqI0pLS/37qHoO6rqfzKi283Ds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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "backward_all = TimeReversalPT(forward_all)\n",
+ "_ = plotting.plot(backward_all, plot_measurements={'M'}, show=False)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "collapsed": false,
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
+ "source": [
+ "and use it in a composed template."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "intermediate = TablePT({'A': [(0, 0), (1, 0)]}, measurements=[('N', 0, 1)])\n",
+ "composed = forward_all @ intermediate @ backward_all\n",
+ "_ = plotting.plot(composed, plot_measurements={'M', 'N'}, show=False)"
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/doc/source/examples/01AdvancedTablePulse.ipynb b/doc/source/examples/01AdvancedTablePulse.ipynb
deleted file mode 100644
index de87a5d57..000000000
--- a/doc/source/examples/01AdvancedTablePulse.ipynb
+++ /dev/null
@@ -1,2517 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Modelling an Advanced TablePulseTemplate\n",
- "\n",
- "[The SimpleTablePulse example](00SimpleTablePulse.ipynb) shows how a simple parametrized ```TablePT``` on one channel can implemented and how the interpolation works.\n",
- "\n",
- "This example demonstrates how to set up a more complex ```TablePT```. This means we will include multiple channels and use expressions for times and voltages.\n",
- "\n",
- "First lets reimplement the pulse from the previous example but this time with a second channel `'B'`, that has the same voltage values but negative. To do this, we extend the entry dict by a second item with the channel ID `'B'` as key and the entry list as value.\n",
- "\n",
- "Then we plot it to see that it actually works."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {
- "scrolled": false
- },
- "outputs": [
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "%matplotlib notebook\n",
- "from qupulse.pulses.plotting import plot\n",
- "from qupulse.pulses import TablePT\n",
- "\n",
- "param_entries = {'A': [(0, 0),\n",
- " ('ta', 'va', 'hold'),\n",
- " ('tb', 'vb', 'linear'),\n",
- " ('tend', 0, 'jump')],\n",
- " 'B': [(0, 0),\n",
- " ('ta', '-va', 'hold'),\n",
- " ('tb', '-vb', 'linear'),\n",
- " ('tend', 0, 'jump')]}\n",
- "mirror_pulse = TablePT(param_entries)\n",
- "\n",
- "parameters = {'ta': 2,\n",
- " 'va': 2,\n",
- " 'tb': 4,\n",
- " 'vb': 3,\n",
- " 'tend': 6}\n",
- "\n",
- "_ = plot(mirror_pulse, parameters, sample_rate=100)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "You may have noticed that we already used an expression in the entry list: `'-va'` and `'-vb'`. Of course we can also do a bit more complex things with these than a simple negation. Let's have a look at the next example where we use some simple mathematical oeprators and built-in functions:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {
- "scrolled": false
- },
- "outputs": [
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "expr_pulse = TablePT({'A': [(0, 'a_0'),\n",
- " ('t_1', 'a_0 + exp(theta)', 'hold'),\n",
- " ('t_2', 'Abs(x_0 - y_0)', 'linear')],\n",
- " 'B': [(0, 'b_0'),\n",
- " ('t_1*(b_0/a_0)', 'b_1', 'linear'),\n",
- " ('t_2', 'b_2')]})\n",
- "_ = plot(expr_pulse, dict(a_0=1.1, theta=2, x_0=0.5, y_0=1, t_1=10, t_2=25, b_0=0.6, b_1=6, b_2=0.4))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- " __Is there a requirement that all channels have the same duration?__\n",
- " \n",
- " No. The shorter channels stay on their last value until the last channel is finished. The duration of the complete pulse template is given as the corresponding expression:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "metadata": {
- "scrolled": false
- },
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Max(t_A, t_B)\n"
- ]
- },
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "param_entries = {'A': [(0, 0),\n",
- " ('t_A/2', 'va', 'hold'),\n",
- " ('t_A', 'vb', 'linear')],\n",
- " 'B': [(0, 0),\n",
- " ('t_B / 2', 'va', 'hold'),\n",
- " ('t_B', 'vb', 'linear')]}\n",
- "\n",
- "c_pulse = TablePT(param_entries)\n",
- "print(c_pulse.duration)\n",
- "_ = plot(c_pulse, dict(t_A=4, t_B=5, va=1, vb=2, t_wait = 2), sample_rate=100)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "As you see channel `'A'` only was defined until 4ns and holds this level to the end of the pulse."
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python [default]",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.7.0"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/doc/source/examples/08Measurements.ipynb b/doc/source/examples/01Measurements.ipynb
similarity index 80%
rename from doc/source/examples/08Measurements.ipynb
rename to doc/source/examples/01Measurements.ipynb
index 22bd3b1e3..2701692b3 100644
--- a/doc/source/examples/08Measurements.ipynb
+++ b/doc/source/examples/01Measurements.ipynb
@@ -21,7 +21,7 @@
"output_type": "stream",
"text": [
"{'N', 'M'}\n",
- "[('M', 0, 't_meas'), ('N', 0, 't_meas/2')]\n"
+ "[('M', ExpressionScalar(0), ExpressionScalar('t_meas')), ('N', ExpressionScalar(0), ExpressionScalar('t_meas/2'))]\n"
]
}
],
@@ -47,7 +47,7 @@
"Note that measurement definitions may not exceed the duration of the pulse they are defined in. Doing so will result in an exception being raised during pulse instantiation.\n",
"Note further that measurements for pulse templates that are empty, e.g. because their length as given by parameters turns out equal to zero, will be discarded during instantiation (without raising an exception).\n",
"\n",
- "When using non-atomic/composite pulse templates such as for example `SequencePulseTemplate`, they will \"inherit\" all the measurements from the subtemplates they are created with (see [Combining PulseTemplates](03xComposedPulses.ipynb) to learn more about composite pulse templates). To avoid name conflicts of measurements from different subtemplates, we can make use of mapping (via [MappingPulseTemplate](05MappingTemplate.ipynb)) to rename the measurements, as the example below demonstrates."
+ "When using non-atomic/composite pulse templates such as for example `SequencePulseTemplate`, they will \"inherit\" all the measurements from the subtemplates they are created with (see [Combining PulseTemplates](00ComposedPulses.ipynb) to learn more about composite pulse templates). To avoid name conflicts of measurements from different subtemplates, we can make use of mapping (via [MappingPulseTemplate](00MappingTemplate.ipynb)) to rename the measurements, as the example below demonstrates."
]
},
{
@@ -59,7 +59,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "{'dbz_fid', 'N', 'charge_scan'}\n"
+ "{'N', 'dbz_fid', 'charge_scan'}\n"
]
}
],
@@ -73,22 +73,8 @@
}
],
"metadata": {
- "kernelspec": {
- "display_name": "Python [default]",
- "language": "python",
- "name": "python3"
- },
"language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.7.0"
+ "name": "python"
}
},
"nbformat": 4,
diff --git a/doc/source/examples/01ParameterConstraints.ipynb b/doc/source/examples/01ParameterConstraints.ipynb
new file mode 100644
index 000000000..ea35fc08b
--- /dev/null
+++ b/doc/source/examples/01ParameterConstraints.ipynb
@@ -0,0 +1,131 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Constraining Parameters\n",
+ "\n",
+ "Often, it is useful to constrain parameters. Either to be in a specific range or even that some relation between parameters is fulfilled. Many pulse templates allow that and accept `parameter_constraints` as a keyword argument. In this example we look at a simple table pulse that ramps a voltage from `v_a` to `v_b` with the ramp time `t_ramp`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from qupulse.pulses import TablePT\n",
+ "from qupulse.plotting import plot\n",
+ "\n",
+ "table_pulse = TablePT({'A': [(0, 'v_a'),\n",
+ " ('t_ramp', 'v_b', 'linear')]})\n",
+ "_ = plot(table_pulse, dict(t_ramp=10, v_a=-1, v_b=1), sample_rate=100)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now, we want to restrict the ramp rate of the pulse to some maximum ramp rate `max_rate` and the ramp time to be larger than 1:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "{'v_b', 'max_rate', 'v_a', 't_ramp'}\n",
+ "Abs(v_a - v_b)/t_ramp < max_rate\n",
+ "t_ramp > 1\n"
+ ]
+ }
+ ],
+ "source": [
+ "table_pulse = TablePT({'A': [(0, 'v_a'),\n",
+ " ('t_ramp', 'v_b', 'linear')]},\n",
+ " parameter_constraints=['Abs(v_a-v_b)/t_ramp < max_rate', 't_ramp>1'])\n",
+ "print(table_pulse.parameter_names)\n",
+ "print(table_pulse.parameter_constraints[0])\n",
+ "print(table_pulse.parameter_constraints[1])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We see that the pulse got the extra parameter `max_rate`. We cannot instantiate this pulse without providing this parameter."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "ParameterNotProvidedException: No value was provided for parameter 'max_rate'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "try:\n",
+ " _ = plot(table_pulse, dict(t_ramp=10, v_a=-1, v_b=1), sample_rate=100)\n",
+ "except Exception as exception:\n",
+ " print('{}: {}'.format(type(exception).__name__, exception))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If one of the constraints is violated an exception is raised:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "ParameterConstraintViolation: The constraint 'Abs(v_a - v_b)/t_ramp < max_rate' is not fulfilled.\n",
+ "Parameters: DictScope(values=frozendict.frozendict({'t_ramp': 10, 'v_a': -1, 'v_b': 1, 'max_rate': 0.1}))\n"
+ ]
+ }
+ ],
+ "source": [
+ "try:\n",
+ " _ = plot(table_pulse, dict(t_ramp=10, v_a=-1, v_b=1, max_rate=0.1), sample_rate=100)\n",
+ "except Exception as exception:\n",
+ " print('{}: {}'.format(type(exception).__name__, exception))"
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/doc/source/examples/01PulseStorage.ipynb b/doc/source/examples/01PulseStorage.ipynb
new file mode 100644
index 000000000..b0889b321
--- /dev/null
+++ b/doc/source/examples/01PulseStorage.ipynb
@@ -0,0 +1,395 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Storing Pulse Templates: `PulseStorage` and Serialization\n",
+ "\n",
+ "So far, we have constructed new pulse templates in code for each session (which were discarded afterwards). We now want to store them persistently in the file system to be able to reuse them in later sessions. For this, qupulse offers us serialization and deserialization using the `PulseStorage` and `StorageBackend` classes.\n",
+ "\n",
+ "The pulse storage manages the (de-)serialization to JSON and requires a storage backend to persistently store the serialized data. This can for example be a `FilesystemBackend` or a `DictBackend`. Let us first use a `DictBackend` to inspect the serialized pulse.\n",
+ "\n",
+ "__Attention:__ Due to the fact that PulseStorage enforces unique identifiers, executing the cells in this notebook out of order or rerunning them will likely result in errors. You will have to restart the Kernel in that case.\n",
+ "\n",
+ "## Single Pulses\n",
+ "First we will have a look at how to store pulses that do not contain other pulse templates. To store a pulse, __the pulse needs to have an identifier__. If you forgot to give the pulse an identifier one can use the `rename` method which returns a new pulse with the requested identifier.\n",
+ "\n",
+ "### Storing"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "{'my_pulse': '{\\n'\n",
+ " ' \"#identifier\": \"my_pulse\",\\n'\n",
+ " ' \"#type\": '\n",
+ " '\"qupulse.pulses.table_pulse_template.TablePulseTemplate\",\\n'\n",
+ " ' \"entries\": {\\n'\n",
+ " ' \"default\": [\\n'\n",
+ " ' [\\n'\n",
+ " ' \"t_begin\",\\n'\n",
+ " ' \"v_begin\",\\n'\n",
+ " ' \"hold\"\\n'\n",
+ " ' ],\\n'\n",
+ " ' [\\n'\n",
+ " ' \"t_end\",\\n'\n",
+ " ' \"v_end\",\\n'\n",
+ " ' \"linear\"\\n'\n",
+ " ' ]\\n'\n",
+ " ' ]\\n'\n",
+ " ' },\\n'\n",
+ " ' \"measurements\": [],\\n'\n",
+ " ' \"parameter_constraints\": []\\n'\n",
+ " '}'}\n"
+ ]
+ }
+ ],
+ "source": [
+ "import pprint\n",
+ "from qupulse.pulses import TablePT\n",
+ "from qupulse.serialization import PulseStorage, DictBackend\n",
+ "\n",
+ "dict_backend = DictBackend()\n",
+ "dict_pulse_storage = PulseStorage(dict_backend)\n",
+ "\n",
+ "table_pulse = TablePT({'default': [('t_begin', 'v_begin', 'hold'),\n",
+ " ('t_end', 'v_end', 'linear')]}, identifier='my_pulse')\n",
+ "\n",
+ "dict_pulse_storage['my_pulse'] = table_pulse\n",
+ "\n",
+ "pprint.pprint(dict_backend.storage)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now to store this in a file system we need to replace the `DictBackend` with a `FilesystemBackend`. The following code will create the file `'./serialized_pulses/my_pulse.json'`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from qupulse.serialization import FilesystemBackend\n",
+ "\n",
+ "filesystem_backend = FilesystemBackend('./serialized_pulses')\n",
+ "file_pulse_storage = PulseStorage(filesystem_backend)\n",
+ "\n",
+ "if 'my_pulse' in file_pulse_storage:\n",
+ " del file_pulse_storage['my_pulse']\n",
+ "\n",
+ "file_pulse_storage['my_pulse'] = table_pulse"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Loading\n",
+ "Now we will load a pulse that is shipped only as a JSON file. It is a single sine with frequency `omega`. Note that loading the same pulse multiple times will give you the same object."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Loading the same pulse multiple times gives you the same object\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import math\n",
+ "from qupulse.plotting import plot\n",
+ "\n",
+ "sine = file_pulse_storage['my_other_pulse']\n",
+ "\n",
+ "_ = plot(sine, {'omega': 2*math.pi}, sample_rate=1000, show=False)\n",
+ "\n",
+ "if sine is file_pulse_storage['my_other_pulse']:\n",
+ " print('Loading the same pulse multiple times gives you the same object')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Composed pulses and the role of identifiers\n",
+ "If we have a pulse that contains other pulses all pulses that have an identifier are stored seperatly. Each `PulseStorage` instance expects that identifiers are unique (see below). Anonymous subpulses are stored together with their parent.\n",
+ "\n",
+ "We will now only use a dictionary as a backend it is easier to see what happens.\n",
+ "\n",
+ "### Storing"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "{'combined': '{\\n'\n",
+ " ' \"#identifier\": \"combined\",\\n'\n",
+ " ' \"#type\": '\n",
+ " '\"qupulse.pulses.sequence_pulse_template.SequencePulseTemplate\",\\n'\n",
+ " ' \"subtemplates\": [\\n'\n",
+ " ' {\\n'\n",
+ " ' \"#type\": '\n",
+ " '\"qupulse.pulses.repetition_pulse_template.RepetitionPulseTemplate\",\\n'\n",
+ " ' \"body\": {\\n'\n",
+ " ' \"#identifier\": \"my_other_pulse\",\\n'\n",
+ " ' \"#type\": \"reference\"\\n'\n",
+ " ' },\\n'\n",
+ " ' \"repetition_count\": \"N_sine\"\\n'\n",
+ " ' },\\n'\n",
+ " ' {\\n'\n",
+ " ' \"#identifier\": \"my_pulse\",\\n'\n",
+ " ' \"#type\": \"reference\"\\n'\n",
+ " ' }\\n'\n",
+ " ' ]\\n'\n",
+ " '}',\n",
+ " 'my_other_pulse': '{\\n'\n",
+ " ' \"#identifier\": \"my_other_pulse\",\\n'\n",
+ " ' \"#type\": '\n",
+ " '\"qupulse.pulses.function_pulse_template.FunctionPulseTemplate\",\\n'\n",
+ " ' \"channel\": \"default\",\\n'\n",
+ " ' \"duration_expression\": \"2*pi/omega\",\\n'\n",
+ " ' \"expression\": \"sin(omega*t)\",\\n'\n",
+ " ' \"measurements\": [],\\n'\n",
+ " ' \"parameter_constraints\": []\\n'\n",
+ " '}',\n",
+ " 'my_pulse': '{\\n'\n",
+ " ' \"#identifier\": \"my_pulse\",\\n'\n",
+ " ' \"#type\": '\n",
+ " '\"qupulse.pulses.table_pulse_template.TablePulseTemplate\",\\n'\n",
+ " ' \"entries\": {\\n'\n",
+ " ' \"default\": [\\n'\n",
+ " ' [\\n'\n",
+ " ' \"t_begin\",\\n'\n",
+ " ' \"v_begin\",\\n'\n",
+ " ' \"hold\"\\n'\n",
+ " ' ],\\n'\n",
+ " ' [\\n'\n",
+ " ' \"t_end\",\\n'\n",
+ " ' \"v_end\",\\n'\n",
+ " ' \"linear\"\\n'\n",
+ " ' ]\\n'\n",
+ " ' ]\\n'\n",
+ " ' },\\n'\n",
+ " ' \"measurements\": [],\\n'\n",
+ " ' \"parameter_constraints\": []\\n'\n",
+ " '}'}\n"
+ ]
+ }
+ ],
+ "source": [
+ "from qupulse.pulses import RepetitionPT, SequencePT\n",
+ "\n",
+ "# anonymous pulse template\n",
+ "repeated_sine = RepetitionPT(sine, 'N_sine')\n",
+ "\n",
+ "my_sequence = SequencePT(repeated_sine, table_pulse, identifier='combined')\n",
+ "\n",
+ "dict_pulse_storage['combined'] = my_sequence\n",
+ "\n",
+ "pprint.pprint(dict_backend.storage)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "As you see, the serialization of 'combined' explicitly contains the anonymous `RepetitionPulseTemplate` but references to 'my_pulse' and 'my_other_pulse' which are stored as separate entries.\n",
+ "\n",
+ "## Pulse registry and unique identifiers\n",
+ "There is the possibility to store pulse templates on construction into a pulse registry. This can be a `PulseStorage`. To set a pulse storage as the default pulse registry call `set_to_default_registry`"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "{'combined': '{\\n'\n",
+ " ' \"#identifier\": \"combined\",\\n'\n",
+ " ' \"#type\": '\n",
+ " '\"qupulse.pulses.sequence_pulse_template.SequencePulseTemplate\",\\n'\n",
+ " ' \"subtemplates\": [\\n'\n",
+ " ' {\\n'\n",
+ " ' \"#type\": '\n",
+ " '\"qupulse.pulses.repetition_pulse_template.RepetitionPulseTemplate\",\\n'\n",
+ " ' \"body\": {\\n'\n",
+ " ' \"#identifier\": \"my_other_pulse\",\\n'\n",
+ " ' \"#type\": \"reference\"\\n'\n",
+ " ' },\\n'\n",
+ " ' \"repetition_count\": \"N_sine\"\\n'\n",
+ " ' },\\n'\n",
+ " ' {\\n'\n",
+ " ' \"#identifier\": \"my_pulse\",\\n'\n",
+ " ' \"#type\": \"reference\"\\n'\n",
+ " ' }\\n'\n",
+ " ' ]\\n'\n",
+ " '}',\n",
+ " 'my_other_pulse': '{\\n'\n",
+ " ' \"#identifier\": \"my_other_pulse\",\\n'\n",
+ " ' \"#type\": '\n",
+ " '\"qupulse.pulses.function_pulse_template.FunctionPulseTemplate\",\\n'\n",
+ " ' \"channel\": \"default\",\\n'\n",
+ " ' \"duration_expression\": \"2*pi/omega\",\\n'\n",
+ " ' \"expression\": \"sin(omega*t)\",\\n'\n",
+ " ' \"measurements\": [],\\n'\n",
+ " ' \"parameter_constraints\": []\\n'\n",
+ " '}',\n",
+ " 'my_pulse': '{\\n'\n",
+ " ' \"#identifier\": \"my_pulse\",\\n'\n",
+ " ' \"#type\": '\n",
+ " '\"qupulse.pulses.table_pulse_template.TablePulseTemplate\",\\n'\n",
+ " ' \"entries\": {\\n'\n",
+ " ' \"default\": [\\n'\n",
+ " ' [\\n'\n",
+ " ' \"t_begin\",\\n'\n",
+ " ' \"v_begin\",\\n'\n",
+ " ' \"hold\"\\n'\n",
+ " ' ],\\n'\n",
+ " ' [\\n'\n",
+ " ' \"t_end\",\\n'\n",
+ " ' \"v_end\",\\n'\n",
+ " ' \"linear\"\\n'\n",
+ " ' ]\\n'\n",
+ " ' ]\\n'\n",
+ " ' },\\n'\n",
+ " ' \"measurements\": [],\\n'\n",
+ " ' \"parameter_constraints\": []\\n'\n",
+ " '}',\n",
+ " 'new_pulse': '{\\n'\n",
+ " ' \"#identifier\": \"new_pulse\",\\n'\n",
+ " ' \"#type\": '\n",
+ " '\"qupulse.pulses.function_pulse_template.FunctionPulseTemplate\",\\n'\n",
+ " ' \"channel\": \"default\",\\n'\n",
+ " ' \"duration_expression\": 1,\\n'\n",
+ " ' \"expression\": 0,\\n'\n",
+ " ' \"measurements\": [],\\n'\n",
+ " ' \"parameter_constraints\": []\\n'\n",
+ " '}'}\n"
+ ]
+ }
+ ],
+ "source": [
+ "from qupulse.pulses import FunctionPT\n",
+ "\n",
+ "dict_pulse_storage.set_to_default_registry()\n",
+ "\n",
+ "new_pulse = FunctionPT(0, 1, identifier='new_pulse')\n",
+ "\n",
+ "pprint.pprint(dict_backend.storage)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "As you see each newly created pulse is put into the pulse storage. Creating a new pulse with the same name will fail:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Oh No!!!\n",
+ "('Pulse with name already exists', 'new_pulse')\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "try:\n",
+ " new_pulse = FunctionPT(0, 1, identifier='new_pulse')\n",
+ "except RuntimeError as err:\n",
+ " print('Oh No!!!')\n",
+ " print(err)\n",
+ " print()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We have to either explicitly overwrite the registry or delete the old pulse from it:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Overwriting the registry works!\n",
+ "Deleting the pulse works, too!\n"
+ ]
+ }
+ ],
+ "source": [
+ "try:\n",
+ " new_pulse = FunctionPT(0, 1, identifier='new_pulse', registry=dict())\n",
+ "except:\n",
+ " raise\n",
+ "else:\n",
+ " print('Overwriting the registry works!')\n",
+ "\n",
+ "del dict_pulse_storage['new_pulse']\n",
+ "try:\n",
+ " new_pulse = FunctionPT(0, 1, identifier='new_pulse')\n",
+ "except:\n",
+ " raise\n",
+ "else:\n",
+ " print('Deleting the pulse works, too!')"
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/doc/source/examples/02CreatePrograms.ipynb b/doc/source/examples/02CreatePrograms.ipynb
new file mode 100644
index 000000000..6e65cbb27
--- /dev/null
+++ b/doc/source/examples/02CreatePrograms.ipynb
@@ -0,0 +1,209 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Instantiating Pulses: Obtaining Pulse Instances From Pulse Templates\n",
+ "\n",
+ "In the previous examples, we have modelled pulses using the basic members of qupulse's `PulseTemplate` class hierarchy. However, these are only templates (or classes) of pulses and may contain parameters so that they cannot be run directly on hardware (this is also the reason why we always have to provide some parameters during plotting). First, we have to instantiate a concrete pulse in a process we call *instantiating*. We achieve this by making use of the `create_program()` method and will need to provide concrete parameter values.\n",
+ "\n",
+ "The example should be mostly self-contained and easy to follow, however, if you started here and don't know what pulse templates are and how to create them, maybe it's best to have a look at [Modelling a Simple TablePulseTemplate](00SimpleTablePulse.ipynb) first.\n",
+ "\n",
+ "To start, let us first create a pulse template with a few parameters and two channels.\n",
+ "## Instantiating a TablePulse"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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1FRJV445161bVx07dtTmzZtVp06dEg0M4Bb4Bl7ZY2LUexdBQEBA0Tbn6+v42WKxSLoyF0eSXnrpJU2dOlWvv/66GjRooKCgIA0dOvSa27r8cRtXt3N1G0VZ5+LFi+rWrZsmT558Tb6rRetGli9frqNHj6pXr175lufm5l6z1wjA9RX5lG6r1eqYxCZJqampysrKUrly5a75Sw/ATVksRToEZKT4+HgFBARo5cqVevzxx29qG+vXr9e9997r+L7Ky8vTvn37VLduXWdGVePGjbVw4UJVq1ZNPj43dSs9zZo1S3/961/13HPP5Vs+YcIEzZo1i1IDFMNN79cMCwtTVFQUhQaAU/n7+2vUqFF65plnNG/ePKWkpOinn34q1hlB8fHxWrFihX788Uf9+uuveuqpp3Ty5EmnZx00aJDOnTun3r17a/PmzUpJSdHy5cv1yCOPKDc394avP336tL788kv169dP9evXz/fo27evFi9e7JiUDODGOFgLwO288MILGjFihMaMGaM6deqoV69e18x1uZ7nn39ejRs3VseOHdWmTRtFRUWpe/fuTs8ZHR2t9evXKzc3Vx06dFCDBg00dOhQlSlTpkhzYebNm6egoKAC5w+1b99eAQEB+uCDD5yeGzAri91ejPMsPVxaWprCwsKUmpqq0NBQQ7NkZmZq7ZotCgmprICAos01QOnmm3lU9b9rp1wvP9lGHrpm0uv/unTpkg4cOKDq1avL39/fRSnhCsX6s908S1o6XGfLNVde6xkKLOKcJZRumVlZSjl9VAnNm97wu8YVivrvN3tqAACAKXhcqXnjjTdUrVo1+fv76/bbb7/mYloAAKB0uqlSc+HCBb377rtKSkpyTGLbtm2bjh496tRw/2v+/PkaPny4xo4dq23btqlhw4bq2LFjsY61AwAAcyr2OYg7d+7UXXfdpbCwMB08eFBPPPGEwsPD9fnnn+vw4cOaN29eSeSUJL366qt64okn9Mgjj0iS3nzzTS1dulSzZ8/Ws88+W2LvWxLOnjisrLQTslzO02Xuxosi8Lc5/+wdlB65ly/p7JnD8vfj+wY3dinbposXTisz/YJbzKkpqmKXmuHDh6t///6aMmWKQkJCHMu7dOmiBx980Knh/ig7O1tbt25VUlKSY5mXl5fuuuuufDey+yObzSabzeZ4npaWVmL5iiv3/fvV3f4f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NDTfAwAAmJOhc2pGjBih/v37X3ed2NhY14QBAAAezdBSExkZqcjISCMjAAAAk/CYs58OHz6sc+fO6fDhw8rNzdWOHTskXbn/VHBwsLHhAACA4Tym1IwZM0Zz5851PG/UqJEkadWqVWrTpo1BqQAAgLvwmLOf5syZI7vdfs2DQgMAACQPKjUAAADXQ6kBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACm4BGl5uDBg3rsscdUvXp1BQQEqEaNGho7dqyys7ONjgYAANyEj9EBimLv3r3Ky8vTW2+9pbi4OO3evVtPPPGEMjIy9PLLLxsdDwAAuAGPKDWdOnVSp06dHM9jY2P122+/aebMmZQalDqX8yRbTq4CjQ4CAG7GI0pNQVJTUxUeHn7ddWw2m2w2m+N5WlpaSccCSoyXxeL4udHENZKk13vdpntvi5blD2MAUFp5xJya/7V//35Nnz5dTz311HXXS05OVlhYmOMRExPjooSA85UL8FVc2YB8y4bO36HqSV+rRfJKbT983qBkAOAeDC01zz77rCwWy3Ufe/fuzfeao0ePqlOnTurRo4eeeOKJ624/KSlJqampjseRI0dK8j8HKFEWi0UvtqqmV1sGq3vDqHxjx1Iv6b7/+1HVnl2qPrM26kTqJYNSAoBxDD38NGLECPXv3/+668TGxjp+PnbsmNq2basWLVro7bffvuH2rVarrFbrrcYE3EqQr0UT762j13s30YEzGRr4wVbtPZHuGF/77zO6I3mlJKlv86oa3aWO/H29jYoLAC5jaKmJjIxUZGRkkdY9evSo2rZtqyZNmui9996Tl5dHHjkDnKp6RJCWDf2TJGnVb6f01Lytys7Nc4zP23BI8zYckiRNvK+B/to0Rl5ezL8BYE4eMVH46NGjatOmjapWraqXX35Zp0+fdoxF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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from qupulse.plotting import plot\n",
+ "from qupulse.pulses import TablePT\n",
+ "\n",
+ "template = TablePT(entries={'A': [(0, 0),\n",
+ " ('ta', 'va', 'hold'),\n",
+ " ('tb', 'vb', 'linear'),\n",
+ " ('tend', 0, 'jump')],\n",
+ " 'B': [(0, 0),\n",
+ " ('ta', '-va', 'hold'),\n",
+ " ('tb', '-vb', 'linear'),\n",
+ " ('tend', 0, 'jump')]}, measurements=[('m', 0, 'ta'),\n",
+ " ('n', 'tb', 'tend-tb')])\n",
+ "\n",
+ "parameters = {'ta': 2,\n",
+ " 'va': 2,\n",
+ " 'tb': 4,\n",
+ " 'vb': 3,\n",
+ " 'tc': 5,\n",
+ " 'td': 11,\n",
+ " 'tend': 6}\n",
+ "_ = plot(template, parameters, sample_rate=100, show=False, plot_measurements={'m', 'n'})\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The `HardwareSetup` class represents the actual hardware and interfaces to the devices in qupulse. It is thus responsible for uploading to and executing pulses on the hardware. To do so it currently expects an instantiated pulse which is represented by `Loop` objects. These can be obtained by plugging the desired parameters into the `create_program` method of your `PulseTemplate` object."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "LOOP 1 times:\n",
+ " ->EXEC MultiChannelWaveform((TableWaveform(channel='A', waveform_table=(TableWaveformEntry(t=0.0, v=0, interp=), TableWaveformEntry(t=2, v=2, interp=), TableWaveformEntry(t=4, v=3, interp=), TableWaveformEntry(t=6, v=0, interp=))), TableWaveform(channel='B', waveform_table=(TableWaveformEntry(t=0.0, v=0, interp=), TableWaveformEntry(t=2, v=-2, interp=), TableWaveformEntry(t=4, v=-3, interp=), TableWaveformEntry(t=6, v=0, interp=))))) 1 times\n",
+ "Defined on frozenset({'B', 'A'})\n",
+ "{'m': (array([0.]), array([2.])), 'n': (array([4.]), array([2.]))}\n"
+ ]
+ }
+ ],
+ "source": [
+ "program = template.create_program(parameters=parameters,\n",
+ " channel_mapping={'A': 'A', 'B': 'B'})\n",
+ "\n",
+ "print(program)\n",
+ "print('Defined on', program[0].waveform.defined_channels)\n",
+ "print(program.get_measurement_windows())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The output shows us that a simple `Loop` object was created which just executes a single waveform without repetitions, just as our `PulseTemplate` specifies. In the `Loop` object all parameter references from the template have been resolved and replaced by the values provided in the `parameters` dictionary, so this is our pulse ready to be executed on the hardware.\n",
+ "\n",
+ "### Mapping Channels and Measurements During Instantiation\n",
+ "\n",
+ "The `channel_mapping` keyword argument allows us to rename channels or to drop them by mapping them to `None`. We can do the same to measurements using the `measurement_mapping` keyword argument."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "LOOP 1 times:\n",
+ " ->EXEC TableWaveform(channel='Y', waveform_table=(TableWaveformEntry(t=0.0, v=0, interp=), TableWaveformEntry(t=2, v=-2, interp=), TableWaveformEntry(t=4, v=-3, interp=), TableWaveformEntry(t=6, v=0, interp=))) 1 times\n",
+ "Defined on {'Y'}\n",
+ "{'foo': (array([0.]), array([2.]))}\n"
+ ]
+ }
+ ],
+ "source": [
+ "program = template.create_program(parameters=parameters,\n",
+ " channel_mapping={'A': None, 'B': 'Y'},\n",
+ " measurement_mapping={'m': 'foo', 'n': None})\n",
+ "print(program)\n",
+ "print('Defined on', program[0].waveform.defined_channels)\n",
+ "print(program.get_measurement_windows())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Instantiating Composed Pulses\n",
+ "\n",
+ "Let's have a brief look at a slightly more complex pulse. Say we want to repeat our previous pulse a few times and follow it up with a brief sine wave on each channel."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "LOOP 1 times:\n",
+ " ->LOOP 4 times:\n",
+ " ->EXEC MultiChannelWaveform((TableWaveform(channel='A', waveform_table=(TableWaveformEntry(t=0.0, v=0, interp=), TableWaveformEntry(t=2, v=2, interp=), TableWaveformEntry(t=4, v=3, interp=), TableWaveformEntry(t=6, v=0, interp=))), TableWaveform(channel='B', waveform_table=(TableWaveformEntry(t=0.0, v=0, interp=), TableWaveformEntry(t=2, v=-2, interp=), TableWaveformEntry(t=4, v=-3, interp=), TableWaveformEntry(t=6, v=0, interp=))))) 1 times\n",
+ " ->EXEC MultiChannelWaveform((FunctionWaveform(duration=TimeType(6283, 1000), expression=ExpressionScalar('sin(t)'), channel='A'), FunctionWaveform(duration=TimeType(6283, 1000), expression=ExpressionScalar('2*sin(t)'), channel='B'))) 1 times\n",
+ "{'m': (array([ 0., 6., 12., 18.]), array([2., 2., 2., 2.])), 'n': (array([ 4., 10., 16., 22.]), array([2., 2., 2., 2.]))}\n"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "C:\\Users\\Simon\\Documents\\git\\qupulse\\qupulse\\plotting.py:186: UserWarning: Sample count 30293/10 is not an integer. Will be rounded (this changes the sample rate).\n",
+ " times, voltages, measurements = render(program,\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from qupulse.pulses import FunctionPT, SequencePT, RepetitionPT, AtomicMultiChannelPT\n",
+ "\n",
+ "repeated_template = RepetitionPT(template, 'n_rep')\n",
+ "sine_template = FunctionPT('sin_a*sin(t)', '2*3.1415')\n",
+ "two_channel_sine_template = AtomicMultiChannelPT(\n",
+ " (sine_template, {'default': 'A'}), \n",
+ " (sine_template, {'default': 'B'}, {'sin_a': 'sin_b'})\n",
+ ")\n",
+ "sequence_template = SequencePT(repeated_template, two_channel_sine_template)\n",
+ "\n",
+ "sequence_parameters = dict(parameters) # we just copy our parameter dict from before\n",
+ "sequence_parameters['n_rep'] = 4 # and add a few new values for the new params from the sine wave\n",
+ "sequence_parameters['sin_a'] = 1\n",
+ "sequence_parameters['sin_b'] = 2\n",
+ "\n",
+ "_ = plot(sequence_template, parameters=sequence_parameters, sample_rate=100, show=False)\n",
+ "sequence_program = sequence_template.create_program(parameters=sequence_parameters, \n",
+ " channel_mapping={'A': 'A', 'B': 'B'})\n",
+ "print(sequence_program)\n",
+ "print(sequence_program.get_measurement_windows())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "As we can see, our `Loop` now contains an inner `Loop` object which repeats a waveform four times and additionally executes another waveform. This reflects the structure of our pulse template. Note also that the single measurement window defined by our pulse template `template` is repeated four times as well in the `Loop` object, according to the number of repetitions of the corresponding pulse.\n",
+ "\n",
+ "Don't worry too much about the inner workings of the `Loop` objects, though. We were just taking a short look at them here. In practice it will be sufficient to just obtain them using the `create_program` method of `PulseTemplate` and pass them on to `HardwareSetup` when required."
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/doc/source/examples/02FunctionPulse.ipynb b/doc/source/examples/02FunctionPulse.ipynb
deleted file mode 100644
index 2d0d53edb..000000000
--- a/doc/source/examples/02FunctionPulse.ipynb
+++ /dev/null
@@ -1,1660 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Modelling Pulses Using Functions And Expressions\n",
- "\n",
- "Assume we want to model a pulse that represents a damped sine function. While we could, in theory, do this using `TablePulseTemplate`s by piecewise linear approximation (cf. [Modelling a Simple TablePulseTemplate](00SimpleTablePulse.ipynb)), this would be a tedious endeavor. A much simpler approach presents itself in the form of the `FunctionPulseTemplate` class of qupulse. Like the `TablePulseTemplate`, a `FunctionPulseTemplate` represents an atomic pulse which will be converted into a waveform for execution. The difference between both is that `FunctionPulseTemplate` accepts a mathematical expression which is parsed and evaluated using `sympy` to sample the waveform instead of the linear interpolation between specified supporting points as it is done in `TablePulseTemplate`.\n",
- "\n",
- "To define the sine function pulse template, we can thus do the following:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "from qupulse.pulses import FunctionPT\n",
- "\n",
- "template = FunctionPT('exp(-t/2)*sin(2*t)', '2*3.1415')\n",
- "\n",
- "%matplotlib notebook\n",
- "from qupulse.pulses.plotting import plot\n",
- "\n",
- "_ = plot(template, sample_rate=100)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The first argument to `FunctionPulseTemplate`'s constructor is the string representation of the formula that the pulse represents. The second argument is used to compute the length of the pulse. In this case, this is simply a constant expression. Refer to [sympy's documentation](http://docs.sympy.org/latest/index.html) to read about the usable operators and functions in the expressions.\n",
- "\n",
- "The `t` is reserved as the free variable of the time domain in the first argument and must be present. Other variables can be used at will and corresponding values have to be passed in as a parameter when instantiating a pulse for execution from the created `FunctionPulseTemplate` object:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {
- "scrolled": false
- },
- "outputs": [
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "param_template = FunctionPT('exp(-t/tau)*sin(phi*t)', 'duration')\n",
- "\n",
- "_ = plot(param_template, {'tau': 4, 'phi': 8, 'duration': 4*3.1415}, sample_rate=100)"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python [default]",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.7.0"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 1
-}
diff --git a/doc/source/examples/03ConstantPulseTemplate.ipynb b/doc/source/examples/03ConstantPulseTemplate.ipynb
deleted file mode 100644
index a2fffaacc..000000000
--- a/doc/source/examples/03ConstantPulseTemplate.ipynb
+++ /dev/null
@@ -1,104 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# The ConstantPulseTemplate\n",
- "\n",
- "The `ConstantPulseTemplate`(or short `ConstantPT`) can be used to define pulse templates with all channels a constant value. The template is easy to define and allows backends to optimize the waveforms on an AWG."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{'A', 'B'}\n",
- "{'A': Expression('10.0000000000000'), 'B': Expression('2.00000000000000')}\n"
- ]
- }
- ],
- "source": [
- "%matplotlib inline\n",
- "from qupulse.pulses import ConstantPT\n",
- "\n",
- "constant_template = ConstantPT(10, {'A': 1., 'B': .2})\n",
- "\n",
- "print(constant_template.defined_channels)\n",
- "print(constant_template.integral)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The pulse template has two channels."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "metadata": {},
- "outputs": [
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "c:\\projects\\qupulse\\qupulse\\pulses\\plotting.py:236: UserWarning: Matplotlib is currently using module://ipykernel.pylab.backend_inline, which is a non-GUI backend, so cannot show the figure.\n",
- " axes.get_figure().show()\n"
- ]
- },
- {
- "data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYIAAAEGCAYAAABo25JHAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/Il7ecAAAACXBIWXMAAAsTAAALEwEAmpwYAAAX5UlEQVR4nO3de3SV9Z3v8feHAI22iheirUSayIFREAwYKCPjZbyircFa68GlrfVYmfbUnl48Hah2eWmdtna6utrpcjzFy1FOMVStVdqitlZa6nRUQBEERNDGGkDF2EFREdDv+WNv6DYkYSfsZ2+S3+e1Vlb283tu3ydiPnluv58iAjMzS1e/ShdgZmaV5SAwM0ucg8DMLHEOAjOzxDkIzMwS17/SBXTX4MGDo66urtJlmJn1KosXL34lImo6mtfrgqCuro5FixZVugwzs15F0vOdzfOlITOzxDkIzMwS5yAwM0ucg8DMLHEOAjOzxDkIzMwS5yAwM0ucg8DMLHEOAjOzxDkIzMwS5yAwM0ucg8DMLHEOAjOzxGUWBJJukfSypKc6mS9J/yZpjaSlksZlVYuZmXUuyzOCW4HJXcw/HRie/5oG3JBhLWZm1onMxiOIiAWS6rpYZAowKyICeETSfpI+FBHrs6jnhVffpKXtjSw2bWZWFsNqPsAh++1V8u1WcmCaIcALBdOt+badgkDSNHJnDQwdOrRHO5u3bD3fue/pHq1rZrYnuPasI7lg4odLvt1eMUJZRMwEZgI0NjZGT7YxpWEIR394/5LWZWZWTkMP3DuT7VYyCNYChxZM1+bbMvHBQdV8cFB1Vps3M+u1Kvn46Fzg0/mnhyYCG7O6P2BmZp3L7IxAUjNwAjBYUitwFTAAICL+DzAPOANYA7wJXJRVLWZm1rksnxo6bxfzA/hCVvs3M7Pi+M1iM7PEOQjMzBLnIDAzS5yDwMwscQ4CM7PEOQjMzBLnIDAzS5yDwMwscQ4CM7PEOQjMzBLnIDAzS5yDwMwscQ4CM7PEOQjMzBLnIDAzS5yDwMwscQ4CM7PEOQjMzBLnIDAzS5yDwMwscQ4CM7PEOQjMzBLnIDAzS5yDwMwscQ4CM7PEOQjMzBLnIDAzS5yDwMwscQ4CM7PEOQjMzBLnIDAzS5yDwMwscQ4CM7PEOQjMzBLnIDAzS5yDwMwscQ4CM7PEZRoEkiZLWiVpjaQZHcwfKmm+pCckLZV0Rpb1mJnZzjILAklVwPXA6cBI4DxJI9st9g3gjogYC0wF/j2reszMrGNZnhFMANZExHMRsQWYA0xpt0wA++Y/DwLWZViPmZl1IMsgGAK8UDDdmm8rdDVwgaRWYB7wxY42JGmapEWSFm3YsCGLWs3MklXpm8XnAbdGRC1wBvD/JO1UU0TMjIjGiGisqakpe5FmZn1ZlkGwFji0YLo231boYuAOgIj4T6AaGJxhTWZm1k6WQbAQGC6pXtJAcjeD57Zb5i/ASQCSjiAXBL72Y2ZWRpkFQURsAy4FHgBWkns6aLmkb0pqyi92GXCJpCeBZuAzERFZ1WRmZjvrn+XGI2IeuZvAhW1XFnxeAUzKsgYzM+tapW8Wm5lZhTkIzMwS5yAwM0ucg8DMLHEOAjOzxDkIzMwS5yAwM0ucg8DMLHG7fKFMUiNwLHAI8BbwFPDbiPhrxrWZmVkZdHpGIOkiSY8DXwf2AlYBLwP/ADwo6TZJQ8tTppmZZaWrM4K9gUkR8VZHMyU1AMPJdRxnZma9VKdBEBHXd7ViRCwpeTVmZlZ2PbpZLOljpS7EzMwqo6dPDY0vaRVmZlYxPQqCiLiq1IWYmVllFPP46Kc7ao+IWaUvx8zMyq2YgWkKLwNVkxta8nHAQWBm1gfsMggi4ouF05L2A+ZkVZCZmZVXT+4RvAHUl7oQMzOrjGLuEfwS2D6gfD9gJHBHlkWZmVn5FHOP4PsFn7cBz0dEa0b1mJlZmRVzj+AP5SjEzMwqo6dvFs8sdSFmZlYZxVwa6shPSlqFmSVp69attLa2snnz5kqX0mdUV1dTW1vLgAEDil6nR0EQEYt7sp6ZWaHW1lb22Wcf6urqkFTpcnq9iKCtrY3W1lbq64t/uLOYp4ZqgOnknhaqLtjhiT0p1Mxsu82bNzsESkgSBx54IBs2bOjWesXcI5gNrCT37sA1QAuwsLsFmpl1xCFQWj35eRYTBAdGxM3A1oj4Q0T8D8BnA2bWZ33mM5/hrrvuqsi+W1paOPLIIzud/8Mf/pDq6mo2btxYsn0WEwRb89/XS/qopLHAASWrwMzMitbc3Mz48eO5++67S7bNYoLgWkmDgMuA/w3cBHylZBWYmVXQrFmzGDNmDEcddRSf+tSndrQvWLCAY445hsMOO2zH2cGmTZs46aSTGDduHKNHj+bee+8Fcn/FH3HEEVxyySWMGjWKU089lbfeyo3ye8IJJzB9+nQmTJjAiBEj+OMf/wjAO++8w9e+9jXGjx/PmDFj+MlPdv0w5rPPPsumTZu49tpraW5uLtnPoJgXyn6V/7gR+MeS7dnMrMA1v1zOinWvlXSbIw/Zl6vOHNXp/OXLl3Pttdfypz/9icGDB/Pqq6/umLd+/Xoefvhhnn76aZqamjjnnHOorq7mF7/4Bfvuuy+vvPIKEydOpKmpCYDVq1fT3NzMjTfeyLnnnsvPf/5zLrjgAgC2bdvGY489xrx587jmmmt48MEHufnmmxk0aBALFy7k7bffZtKkSZx66qldXuOfM2cOU6dO5dhjj2XVqlW89NJLHHzwwbv9c+r0jEDSNyR1eglI0okestLMerOHHnqIT37ykwwePBiAAw7426+8s846i379+jFy5EheeuklIPd45uWXX86YMWM4+eSTWbt27Y559fX1NDQ0AHD00UfT0tKyY1tnn332Tu2/+c1vmDVrFg0NDXzkIx+hra2N1atXd1lvc3MzU6dOpV+/fnziE5/gzjvvLMWPocszgmXALyVtJjf+wAZyj48OBxqAB4Fvl6QKM0teV3+5V8L73ve+HZ8jcv1uzp49mw0bNrB48WIGDBhAXV3djpfhCpevqqracWmocF5VVRXbtm3bsc0f//jHnHbaae/Zb2GAFFq2bBmrV6/mlFNOAWDLli3U19dz6aWX7uaRdnFGEBH3RsQk4HPAcqAKeA34KTAhIr4SEd17WNXMbA9y4okncuedd9LW1gbwnktDHdm4cSMHHXQQAwYMYP78+Tz//PM93vdpp53GDTfcwNatuedxnnnmGd54441Ol29ububqq6+mpaWFlpYW1q1bx7p163arhu2KuUewGuj6fMXMrBcaNWoUV1xxBccffzxVVVWMHTuWW2+9tdPlzz//fM4880xGjx5NY2Mjhx9+eI/3/dnPfpaWlhbGjRtHRFBTU8M999zT6fJz5sxh3rx572n7+Mc/zpw5c5g+fXqP6wDQ9lOe3qKxsTEWLVpU6TLMrARWrlzJEUccUeky+pyOfq6SFkdEY0fL96j30WJJmixplaQ1kmZ0ssy5klZIWi7p9izrMTOznfW099FdklQFXA+cArQCCyXNjYgVBcsMB74OTIqIv0o6KKt6zMysY7s8I5A0QtLvJD2Vnx4j6RtFbHsCsCYinouILeQGvJ/SbplLgOsj4q8AEfFy98o3M7PdVcyloRvJ/dW+FSAilgJTi1hvCPBCwXRrvq3QCGCEpP+Q9IikyR1tSNI0SYskLepur3pmZta1YoJg74h4rF3bthLtvz+59xJOAM4DbpS0X/uFImJmRDRGRGNNTU2Jdm1mZlBcELwiaRgQAJLOAdYXsd5a4NCC6dp8W6FWYG5EbI2IPwPPkAsGMzMrk2KC4AvkhqY8XNJa4MvA54tYbyEwXFK9pIHkLifNbbfMPeTOBpA0mNyloueKKdzMLCt7YjfULS0t7LXXXjQ0NHDUUUdxzDHHsGrVqpLsc5dBkL/ZezJQAxweEf8QES1FrLcNuBR4gNzANndExHJJ35TUlF/sAaBN0gpgPvC1iGjr4bGYmfVpw4YNY8mSJTz55JNceOGFfPvbpenlp5inhr4q6avAPwGX5KcvltSwq3UjYl5EjIiIYRHxL/m2KyNibv5zRMRXI2JkRIyOiDm7eTxmZt3Sm7qhLvTaa6+x//77l+JHUNR7BI35r1/mpz8GLAU+J+nOiPheSSoxs7TdNwNeXFbabX5wNJz+3U5n97ZuqJ999lkaGhp4/fXXefPNN3n00UdL8mMqJghqgXERsQlA0lXAr4HjgMWAg8DMeqWedkO9YMEC+vXrt9vdUC9dunTH2cbGjRtZvXo1I0aM6LTe7ZeGAH72s58xbdo07r///t3+ORQTBAcBbxdMbwUOjoi3JL3dyTpmZt3TxV/ulbCndUPdXlNTExdddFH3D6wDxTw1NBt4VNJV+bOB/wBul/R+YEXXq5qZ7bl6UzfU7T388MMMGzasx/svVEw31N+SdD9wTL7pcxGxvfvP80tShZlZBfSmbqjhb/cIIoKBAwdy00039Xj/hYruhjrfIVz19umI+EtJKugmd0Nt1ne4G+pslLwbaklNklYDfwb+kP9+XwlqNTOzPUAx9wi+BUwEnomIeuBk4JFMqzIzs7IpJgi25t/27SepX0TMJ/degZmZ9QHFPD76X5I+ACwAZkt6GSj+1raZWRciosuXqKx7ejL8cDFnBFOAN4GvAPcDz5J7u9jMbLdUV1fT1tbWo19etrOIoK2tjerq6l0vXKCYM4IrI2I68C5wG4Ck64Dp3a7SzKxAbW0tra2teMCp0qmurqa2trZb6xQTBKew8y/90ztoMzPrlgEDBlBfX1/pMpLXaRBI+jzwP4HDJC0tmLUPubeLzcysD+jqjOB2cu8LfAeYUdD+ekR0/R62mZn1Gl0FQRXwGrkRyt5D0gEOAzOzvqGrIFhMfpxioP2zXQEclklFZmZWVp0GQf4tYjMz6+OKeWqI/BjDx+Unfx8Rv8quJDMzK6diOp37LvAlcmMPrAC+JKk0IyabmVnFFXNGcAbQEBHvAki6DXgCuDzLwszMrDyK6WICYL+Cz4MyqMPMzCqkmDOC7wBPSJpP7umh43jvewVmZtaLdfVm8fXA7RHRLOn3wPj8rOkR8WI5ijMzs+x1dUbwDPB9SR8C7gCaI+KJ8pRlZmbl0uk9goj4UUT8PXA80AbcIulpSVdJGlG2Cs3MLFO7vFkcEc9HxHURMRY4DzgLWJl1YWZmVh7FvEfQX9KZkmaT64RuFXB25pWZmVlZdHWz+BRyZwBnAI8Bc4BpEeFhKs3M+pCubhZ/nVxX1JdFxF/LVI+ZmZVZV53OnVjOQszMrDKKfbPYzMz6KAeBmVniHARmZolzEJiZJc5BYGaWuEyDQNJkSaskrZHUaY+lkj4hKSQ1ZlmPmZntLLMgkFQFXA+cDowEzpM0soPl9iE3AtqjWdViZmady/KMYAKwJiKei4gt5N5MntLBct8CrgM2Z1iLmZl1IssgGAK8UDDdmm/bQdI44NCI+HVXG5I0TdIiSYs2bNhQ+krNzBJWsZvFkvoBPwAu29WyETEzIhojorGmpib74szMEpJlEKwFDi2Yrs23bbcPcCTwe0ktwERgrm8Ym5mVV5ZBsBAYLqle0kBgKjB3+8yI2BgRgyOiLiLqgEeApohYlGFNZmbWTmZBEBHbgEuBB8gNZHNHRCyX9E1JTVnt18zMuqerbqh3W0TMA+a1a7uyk2VPyLIWMzPrmN8sNjNLnIPAzCxxDgIzs8Q5CMzMEucgMDNLnIPAzCxxDgIzs8Q5CMzMEucgMDNLnIPAzCxxDgIzs8Q5CMzMEucgMDNLnIPAzCxxDgIzs8Q5CMzMEucgMDNLnIPAzCxxDgIzs8Q5CMzMEucgMDNLnIPAzCxxDgIzs8Q5CMzMEucgMDNLnIPAzCxxDgIzs8Q5CMzMEucgMDNLnIPAzCxxDgIzs8Q5CMzMEucgMDNLnIPAzCxxDgIzs8Q5CMzMEucgMDNLXKZBIGmypFWS1kia0cH8r0paIWmppN9J+nCW9ZiZ2c76Z7VhSVXA9cApQCuwUNLciFhRsNgTQGNEvCnp88D3gP+eSUFb38p9mZn1VgP2hgHVJd9sZkEATADWRMRzAJLmAFOAHUEQEfMLln8EuCCzah6bCb+9MrPNm5ll7qM/gPEXl3yzWQbBEOCFgulW4CNdLH8xcF9HMyRNA6YBDB06tGfV1B8Hk6/r2bpmZnuCoRMz2WyWQVA0SRcAjcDxHc2PiJnATIDGxsbo0U4OGZv7MjOz98gyCNYChxZM1+bb3kPSycAVwPER8XaG9ZiZWQeyfGpoITBcUr2kgcBUYG7hApLGAj8BmiLi5QxrMTOzTmQWBBGxDbgUeABYCdwREcslfVNSU36xfwU+ANwpaYmkuZ1szszMMpLpPYKImAfMa9d2ZcHnk7Pcv5mZ7ZrfLDYzS5yDwMwscQ4CM7PEOQjMzBLnIDAzS5yDwMwscQ4CM7PEOQjMzBLnIDAzS5yDwMwscQ4CM7PEOQjMzBKniJ6N81IpkjYAz/dw9cHAKyUspzfwMafBx5yG3TnmD0dETUczel0Q7A5JiyKisdJ1lJOPOQ0+5jRkdcy+NGRmljgHgZlZ4lILgpmVLqACfMxp8DGnIZNjTuoegZmZ7Sy1MwIzM2vHQWBmlrhkgkDSZEmrJK2RNKPS9WRN0qGS5ktaIWm5pC9VuqZykFQl6QlJv6p0LeUgaT9Jd0l6WtJKSX9f6ZqyJukr+X/TT0lqllRd6ZpKTdItkl6W9FRB2wGSfitpdf77/qXaXxJBIKkKuB44HRgJnCdpZGWrytw24LKIGAlMBL6QwDEDfAlYWekiyuhHwP0RcThwFH382CUNAf4X0BgRRwJVwNTKVpWJW4HJ7dpmAL+LiOHA7/LTJZFEEAATgDUR8VxEbAHmAFMqXFOmImJ9RDye//w6uV8QQypbVbYk1QIfBW6qdC3lIGkQcBxwM0BEbImI/6poUeXRH9hLUn9gb2BdhespuYhYALzarnkKcFv+823AWaXaXypBMAR4oWC6lT7+S7GQpDpgLPBohUvJ2g+BfwberXAd5VIPbAD+b/5y2E2S3l/porIUEWuB7wN/AdYDGyPiN5WtqmwOjoj1+c8vAgeXasOpBEGyJH0A+Dnw5Yh4rdL1ZEXSx4CXI2JxpWspo/7AOOCGiBgLvEEJLxfsifLXxaeQC8FDgPdLuqCyVZVf5J77L9mz/6kEwVrg0ILp2nxbnyZpALkQmB0Rd1e6noxNApoktZC79HeipJ9WtqTMtQKtEbH9TO8ucsHQl50M/DkiNkTEVuBu4JgK11QuL0n6EED++8ul2nAqQbAQGC6pXtJAcjeX5la4pkxJErlrxysj4geVridrEfH1iKiNiDpy/30fiog+/ZdiRLwIvCDp7/JNJwErKlhSOfwFmChp7/y/8ZPo4zfIC8wFLsx/vhC4t1Qb7l+qDe3JImKbpEuBB8g9ZXBLRCyvcFlZmwR8ClgmaUm+7fKImFe5kiwDXwRm5//AeQ64qML1ZCoiHpV0F/A4uSfjnqAPdjUhqRk4ARgsqRW4CvgucIeki8l1xX9uyfbnLibMzNKWyqUhMzPrhIPAzCxxDgIzs8Q5CMzMEucgMDNLnIPAkiLpQElL8l8vSlqb/7xJ0r9ntM8vS/p0D9YbKGlBvk8ds8z48VFLlqSrgU0R8f0M99Gf3DPv4yJiWw/Wv4pch4mzS16cWZ7PCMwASSdsH8NA0tWSbpP0R0nPSzpb0vckLZN0f77rDiQdLekPkhZLemD76//tnAg8vj0EJP1e0nWSHpP0jKRj8+2j8m1LJC2VNDy//j3A+Zn/ACxpDgKzjg0j90u8CfgpMD8iRgNvAR/Nh8GPgXMi4mjgFuBfOtjOJKB9R3j9I2IC8GVyb4wCfA74UUQ0AI3k+hECeAoYX6JjMuuQrz2adey+iNgqaRm5bknuz7cvA+qAvwOOBH6b6/KGKnLdIrf3IXbuC2d7B4CL89sC+E/givyYCndHxGqAiHhH0hZJ++THlTArOQeBWcfeBoiIdyVtjb/dTHuX3P83ApZHxK6GhnwLaD+U4tv57+/kt0VE3C7pUXID68yT9E8R8VB+ufcBm3fraMy64EtDZj2zCqjZPkawpAGSRnWw3Ergv+1qY5IOA56LiH8j16vkmHz7gcAr+S6XzTLhIDDrgfyQp+cA10l6ElhCx/3i30duOMldORd4Kt9T7JHArHz7PwK/3t16zbrix0fNMibpF8A/b7/u38117wZmRMQzpa/MLMdnBGbZm0HupnG35McYuMchYFnzGYGZWeJ8RmBmljgHgZlZ4hwEZmaJcxCYmSXOQWBmlrj/D7nLZAftp6qcAAAAAElFTkSuQmCC\n",
- "text/plain": [
- ""
- ]
- },
- "metadata": {
- "needs_background": "light"
- },
- "output_type": "display_data"
- }
- ],
- "source": [
- "from qupulse.pulses.plotting import plot\n",
- "\n",
- "_ = plot(constant_template, sample_rate=100)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": []
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.8.6"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 4
-}
diff --git a/doc/source/examples/03FreeInductionDecayExample.ipynb b/doc/source/examples/03FreeInductionDecayExample.ipynb
new file mode 100644
index 000000000..365eff2aa
--- /dev/null
+++ b/doc/source/examples/03FreeInductionDecayExample.ipynb
@@ -0,0 +1,201 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Free Induction Decay - A Real Use Case\n",
+ "\n",
+ "The following will give an example of a complex pulse using many of the features discussed in the previous tutorial examles: We will use two channels, parameters and parameter constraints, parameterized measurements and atomic and non-atomic pulse templates. This is based on real experiments. To see another, a bit more artificial example for a pulse setup use case that offers more verbose explanations, see [Gate Configuration - A Full Use Case](03GateConfigurationExample.ipynb).\n",
+ "\n",
+ "We start by creating some atomic pulse templates using `PointPT` which will be the building blocks for the more complex pulse structure we have in mind."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from qupulse.pulses import PointPT, SequencePT, ForLoopPT, RepetitionPT, MappingPT\n",
+ "import qupulse.pulses.plotting\n",
+ "import numpy as np\n",
+ "import sympy as sp\n",
+ "from sympy import sympify as S\n",
+ "\n",
+ "channel_names = ['RFX', 'RFY']\n",
+ "\n",
+ "S_init = PointPT([(0, 'S_init'),\n",
+ " ('t_init', 'S_init')],\n",
+ " channel_names=channel_names, identifier='S_init')\n",
+ "\n",
+ "meas_wait = PointPT([(0, 'meas'),\n",
+ " ('t_meas_wait', 'meas')],\n",
+ " channel_names=channel_names)\n",
+ "\n",
+ "adprep = PointPT([(0, 'meas'),\n",
+ " ('t_ST_prep', 'ST_plus - ST_jump/2', 'linear'),\n",
+ " ('t_ST_prep', 'ST_plus + ST_jump/2'),\n",
+ " ('t_op', 'op', 'linear')],\n",
+ " parameter_constraints=['Abs(ST_plus - ST_jump/2 - meas) <= Abs(ST_plus - meas)',\n",
+ " 'Abs(ST_plus - ST_jump/2 - meas)/t_ST_prep <= max_ramp_speed',\n",
+ " 'Abs(ST_plus + ST_jump/2 - op)/Abs(t_ST_prep-t_op) <= max_ramp_speed'],\n",
+ " channel_names=channel_names, identifier='adprep')\n",
+ "\n",
+ "adread = PointPT([(0, 'op'),\n",
+ " ('t_ST_read', 'ST_plus + ST_jump/2', 'linear'),\n",
+ " ('t_ST_read', 'ST_plus - ST_jump/2'),\n",
+ " ('t_meas_start', 'meas', 'linear'),\n",
+ " ('t_meas_start + t_meas_duration', 'meas')],\n",
+ " parameter_constraints=['Abs(ST_plus - ST_jump/2 - meas) <= Abs(ST_plus - meas)',\n",
+ " 'Abs(ST_plus - ST_jump/2 - meas)/t_ST_read <= max_ramp_speed',\n",
+ " 'Abs(ST_plus + ST_jump/2 - op)/Abs(t_ST_read-t_op) <= max_ramp_speed'],\n",
+ " channel_names=channel_names, identifier='adread',\n",
+ " measurements=[('m', 't_meas_start', 't_meas_duration')])\n",
+ "\n",
+ "free_induction = PointPT([(0, 'op-eps_J'),\n",
+ " ('t_fid', 'op-eps_J')], channel_names=channel_names)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "In the next step, we combine our building blocks into more complex pulses step by step.\n",
+ "We first define our core functionality pulse template `stepped_free_induction`.\n",
+ "The pulse template `pulse` surrounds our functionality with pulses to reset/initialize our qubit and allow for data acquisition.\n",
+ "We will use `pulse` in a `ForLoopPT` `looped_pulse` to perform a parameter sweep. Our final pulse template `experiment` repeats this whole thing a number of times to allow for statistical aggregating of measurement data and represents the complete pulse template for our experiment."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "\n",
+ "\n",
+ "stepped_free_induction = MappingPT(free_induction, parameter_mapping={'t_fid': 't_start + i_fid*t_step'}, allow_partial_parameter_mapping=True)\n",
+ "\n",
+ "pulse = SequencePT(S_init, meas_wait, adprep, stepped_free_induction, adread)\n",
+ "\n",
+ "looped_pulse = ForLoopPT(pulse, loop_index='i_fid', loop_range='N_fid_steps')\n",
+ "\n",
+ "experiment = RepetitionPT(looped_pulse, 'N_repetitions', identifier='free_induction_decay')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(experiment.parameter_names)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let's use some reasonable (but low) values for our parameters and plot our `experiment` pulse (we set the number of repeititions of `looped_pulse` only to 2 so that the plot does not get too stuffed).\n",
+ "\n",
+ "Note that we provide numpy arrays of length 2 for some parameters to assign different values for different channels (see also [The PointPulseTemplate](00PointPulse.ipynb))."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%matplotlib notebook\n",
+ "\n",
+ "example_values = dict(meas=[0, 0],\n",
+ " op=[5, -5],\n",
+ " eps_J=[1, -1],\n",
+ " ST_plus=[2.5, -2.5],\n",
+ " S_init=[-1, -1],\n",
+ " ST_jump=[1, -1],\n",
+ " max_ramp_speed=0.3,\n",
+ " \n",
+ " t_init=5,\n",
+ " \n",
+ " t_meas_wait = 1,\n",
+ " \n",
+ " t_ST_prep = 10,\n",
+ " t_op = 20,\n",
+ " \n",
+ " t_ST_read = 10,\n",
+ " t_meas_start = 20,\n",
+ " t_meas_duration=5,\n",
+ " \n",
+ " t_start=0,\n",
+ " t_step=5,\n",
+ " N_fid_steps=5, N_repetitions=2)\n",
+ "\n",
+ "from qupulse.pulses.plotting import plot\n",
+ "\n",
+ "_ = plot(experiment, example_values)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can clearly make out the many repetitions of our basic functionality pulse and also the varying duration between the voltage peaks due to our parameter sweep (as well as the two-fold repetition of the sweep itself).\n",
+ "\n",
+ "Let's also quickly plot only a single repetition by setting according parameters for our `experiment` pulse template."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "example_values['N_fid_steps'] = 1\n",
+ "example_values['N_repetitions'] = 1\n",
+ "example_values['t_start'] = 5\n",
+ "\n",
+ "_ = plot(experiment, example_values)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "As a last step we will save the pulse and some example parameters so we can use it in other examples."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import json\n",
+ "from qupulse.serialization import FilesystemBackend, PulseStorage\n",
+ "\n",
+ "pulse_storage = PulseStorage(FilesystemBackend('./serialized_pulses'))\n",
+ "\n",
+ "# overwrite all pulses explicitly\n",
+ "pulse_storage.overwrite('adprep', adprep)\n",
+ "pulse_storage.overwrite('S_init', S_init)\n",
+ "pulse_storage.overwrite('adread', adread)\n",
+ "pulse_storage.overwrite('free_induction_decay', experiment)\n",
+ "\n",
+ "with open('parameters/free_induction_decay.json', 'w') as parameter_file:\n",
+ " json.dump(example_values, parameter_file)\n",
+ "\n",
+ "print('Successfully saved pulse and example parameters')"
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/doc/source/examples/03GateConfigurationExample.ipynb b/doc/source/examples/03GateConfigurationExample.ipynb
new file mode 100644
index 000000000..f20bf30d7
--- /dev/null
+++ b/doc/source/examples/03GateConfigurationExample.ipynb
@@ -0,0 +1,334 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Gate Configuration - A Full Use Case"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "collapsed": true
+ },
+ "source": [
+ "An example for a real use case of qupulse is the search for and evaluation of parameters for pulses that represent quantum gate operations on a toy example. To see an example closer to reality but less verbose in explanations, please see [Free Induction Decay - A Real Use Case](03FreeInductionDecayExample.ipynb).\n",
+ "\n",
+ "## Description of the Experiment\n",
+ "The experiment will typically involve a set of gate pulses $G_j, 0 \\leq j < N_{Gates}$.\n",
+ "\n",
+ "The template for a gate pulse $G_j$ is a sequence of $\\epsilon_i, 0 \\leq i < N_{G_j}$ voltage levels held for time $\\Delta t = 1$ ns as illustrated in the figure below (with $N_{G_j} = 7$).\n",
+ "\n",
+ "\n",
+ "\n",
+ "The experiment defines a number of sequences $S_k, 0 \\leq k < N_{Sequences}$ of the $G_j$ as $$S_k = (G_{m_k(1)}, G_{m_k(2)}, \\dots, G_{m_k(N_{S_k})})$$ where $N_{S_k}$ is the length of sequence $k$ and $m_k(i): \\{0, \\dots, N_{S_k} - 1\\} \\rightarrow \\{0, \\dots, N_{Gates} - 1\\}$ is a function that maps an index $i$ to the $m_k(i)$-th gate of sequence $S_k$ and thus fully describes the sequence. (These sequences express the sequential application of the gates to the qubit. In terms of quantum mathematics they may rather be expressed as multiplication of the matrices describing the unitary transformations applied by the gates: $S_k = \\prod_{i=N_{S_k} - 1}^{0} G_{m_k(i)} = G_{(N_{S_k} - 1)} \\cdot \\dots \\cdot G_{1} \\cdot G_{0}$.)\n",
+ "\n",
+ "Measuring and analysing the effects of these sequences on the qubit's state to derive parameters $\\epsilon_i$ for gate pulses that achieve certain state transformations is the goal of the experiment.\n",
+ "\n",
+ "To this end, every sequence must be extended by some preceeding initialization pulse and a succeeding measurement pulse. Furthermore, due to hardware constraints in measuring, all sequences must be of equal length (which is typically 4 µs). Thus, some sequences require some wait time before initialization to increase their playback duration. These requirements give raise to extended sequences $S_k'$ of the form:\n",
+ "$$S_k' = I_{p(k)} | W_k | S_k | M_{q(k)}$$\n",
+ "where the functions $p(k)$ and $q(k)$ respectively select some initialization pulse $I_{p(k)} \\in \\{I_1, I_2, \\dots\\}$ and measurement pulse $M_{q(k)} \\in \\{M_1, M_2, \\dots\\}$ for sequence $k$ and $W_k$ is the aforementioned wait pulse. The '|' denote concatenation of pulses, i.e., sequential execution.\n",
+ "\n",
+ "Since measurement of quantum state is a probabilistic process, many measurements of the effect of a single sequence must be made to reconstruct the resulting state of the qubit. Thus, the experiment at last defines scanlines (typically of duration 1 second), which are sequences of the $S_k'$. (These simply represent batches of sequences to configure playback and measurement systems and have no meaning to the experiment beyond these practical considerations.)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Implementation Using qupulse\n",
+ "\n",
+ "We now want to illustrate how to setup the experiment described above using qupulse. Let us assume the experiment considers only two different gates pulses ($N_{Gates} = 2$). We further assume that $N_{G_1} = 20$ and $N_{G_2} = 18$. We define them using instances of `TablePulseTemplate`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from qupulse.pulses import TablePT\n",
+ "\n",
+ "delta_t = 1 # assuming that delta_t is 1 elementary time unit long in pulse defintions, i.e. 1 time unit = 1 ns\n",
+ "\n",
+ "gate_0 = TablePT({0: [(i*delta_t, 'gate_0_eps_' + str(i))\n",
+ " for i in range(19)]})\n",
+ " \n",
+ "gate_1 = TablePT({0: [(i*delta_t, 'gate_2_eps_' + str(i))\n",
+ " for i in range(17)]})\n",
+ " \n",
+ "gates = [gate_0, gate_1]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We thus obtain two `TablePulseTemplate` of the desired form with parameters 'gate_1_eps_1' to 'gate_1_eps_20' and 'gate_2_eps_1' to 'gate_2_eps_18'.\n",
+ "\n",
+ "Next, we will define sequences as `SequncePulseTemplate` objects. We assume that the mapping functions $m_k$ are given as a 2 dimensional array (which impilicitly also defines $N_{Sequences}$ and $N_{S_k}$), e.g.:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "m = [\n",
+ " [0, 1, 0, 0, 0, 1, 1, 0, 1], # m_0(i)\n",
+ " [1, 1, 0, 0, 1, 0], # m_1(i)\n",
+ " [1, 0, 0, 1, 1, 0, 0, 1] #m_2(i)\n",
+ " ]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The `SequencePulseTemplate` objects can now easily be constructed:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from qupulse.pulses import SequencePT\n",
+ "\n",
+ "# SequencePulseTemplate requires a definition of parameter mappings from parameters passed into the\n",
+ "# SequencePulseTemplate object to its subtemplates. In this case, we want parameters to map 1 to 1\n",
+ "# and thus create an identity mapping of parameter names for both gates using python list/set/dict comprehension\n",
+ "epsilon_mappings = [\n",
+ " {param_name: param_name for param_name in gates[0].parameter_names},\n",
+ " {param_name: param_name for param_name in gates[1].parameter_names}\n",
+ "]\n",
+ "all_epsilons = gates[0].parameter_names | gates[1].parameter_names\n",
+ "\n",
+ "sequences = []\n",
+ "for m_k in m:\n",
+ " subtemplates = []\n",
+ " \n",
+ " sequences.append(SequencePT(*(gates[g_ki] for g_ki in m_k)))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We end up with a list `sequences` which contains the sequences described by our $m$ as qupulse pulse templates.\n",
+ "\n",
+ "To visualize our progress, let us plot our two gates and the second sequence with some random values between $-5$ and $5$ for the $\\epsilon_i$:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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AABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABLINQAAABL8JpQs3DhQl1//fUKDQ1VVFSURo4cqSNHjni6LAAA0Eh4TajZsmWLpk6dqu3btys9PV1lZWUaNGiQLl++7OnSAABAI9DM0wXY65NPPqnyfNWqVYqKitKePXv0m9/8psZlSkpKVFJSYnteUFDg0hoBAIDneM1IzS/l5+dLkiIiImpts3DhQoWFhdke8fHx7ioPAAC4mVeGmoqKCk2fPl033nijrr322lrbzZkzR/n5+bbHqVOn3FglAABwJ685/PRzU6dO1cGDB7V169Y62wUEBCggIMBNVQEAAE/yulAzbdo0/f3vf9fnn3+uNm3aeLocAADQSHhNqDFNUw888IDee+89bd68We3atfN0SQAAoBHxmlAzdepU/e///q8++OADhYaGKjc3V5IUFhamoKAgD1cHoDamaaq4rNwt6wry85VhGG5ZF4DGx2tCzfLlyyVJ/fv3rzL9tddeU1pamvsLAlAv0zR1+4pM7cm+4Jb1pSSEa+3kPgQboInymlBjmqanSwDgoOKycrcFGknanX1BxWXlCvb3mo82AE7EOx+AW+yem6pgf1+X9F1UWq6UJze6pG8A3oNQA8Atgv19GUEB4FJeefE9AACAXyLUAAAASyDUAAAASyDUAAAASyDUAAAASyDUAAAASyDUAAAAS3D4ohElJSXasWOHsrOzVVRUpFatWqlHjx7cYBIAAHiU3aFm27Ztev755/V///d/Kisrs91I8vvvv1dJSYmSkpJ03333afLkyQoNDXVlzQAAANXYdfhpxIgRGjNmjBITE/Xpp5+qsLBQ58+f17fffquioiJ9/fXXmjt3rjIyMnT11VcrPT3d1XUDAABUYddIzbBhw7Ru3Tr5+fnVOD8pKUlJSUkaP368Dh06pJycHKcWCQAAUB+7Qs39999vd4edO3dW586dG1wQAABAQ/DtJwAAYAlOCzXjx4/XwIEDndUdAACAQxz+SndtWrduLR8fBn4AAIBnOC3ULFiwwFldAQAAOIyhFQAAYAkOj9Tcc889dc5fuXJlg4sBAABoKIdDzYULF6o8Lysr08GDB3Xx4kVOFAYAAB7jcKh57733qk2rqKjQlClT1L59e6cUBQCNlWmaKi4rd8u6gvx8ZRiGW9YFWIFTThT28fHRzJkz1b9/fz388MPO6BIAGh3TNHX7ikztyb5Qf2MnSEkI19rJfQg2gJ2cdqLw8ePH9eOPPzqrOwBodIrLyt0WaCRpd/YFt40KAVbg8EjNzJkzqzw3TVM5OTlav369xo8f77TCmhqGtAHvsntuqoL9fV3Sd1FpuVKe3OiSvgErczjU7Nu3r8pzHx8ftWrVSs8880y934xCzRjSBrxPsL+vgv2ddqkvAE7g8Dty06ZNrqijSfPEkPb5y6Uu+y+zEiNCAAB34t+MRsZdQ9ruGNpmRAgA4E5OCzWPPPKIcnNzufjeFXLlkHaQn69SEsK1202jQpUnOTJEDwBwB6f9tTl9+rROnTrlrO7gAoZhaO3kPi4/IZmTHAEAnuC0UPP66687qyu4kGEYjJwAACyJG1oCAABLaNC/7JcvX9aWLVt08uRJlZaWVpn34IMPOqUwAAAARzToOjW33HKLioqKdPnyZUVEROjcuXMKDg5WVFQUoQYAAHiEw4efZsyYoeHDh+vChQsKCgrS9u3blZ2drV69emnJkiWuqBEAAKBeDoea/fv3a9asWfLx8ZGvr69KSkoUHx+vxYsX65FHHnFFjQAAAPVyONT4+fnJx+enxaKionTy5ElJUlhYGF/pBgAAHuPwOTU9evTQrl27lJycrJtuuknz5s3TuXPn9Ne//lXXXnutK2oEAACol8MjNQsWLFBsbKwk6amnnlJ4eLimTJmi7777Ti+//LLTCwQAALCHwyM1KSkptp+joqL0ySefOLUgAACAhuDiewAAwBLsCjVDhgzR9u3b621XWFioRYsW6cUXX7ziwgAAABxh1+GnUaNG6bbbblNYWJiGDx+ulJQUxcXFKTAwUBcuXNChQ4e0detWffTRRxo2bJiefvppV9cNAABQhV2h5t5779Vdd92ltWvXavXq1Xr55ZeVn58v6acbJHbu3FmDBw/Wrl27dM0117i0YAAAgJrYfaJwQECA7rrrLt11112SpPz8fBUXF+uqq66Sn5+fywoEAACwR4NuaCn9dLG9sLAwZ9YCAADQYHz7CQAAWAKhBgAAWAKhBgAAWEKDz6kBAKCxKiotd/k6gvx8ZRiGy9cD+zUo1Fy8eFHvvPOOjh8/rv/6r/9SRESE9u7dq+joaLVu3drZNQIA4JCUJze6fh0J4Vo7uQ/BphFxONR8+eWXSk1NVVhYmE6cOKFJkyYpIiJC7777rk6ePKk33njDFXUCAFCnID9fpSSEa3f2Bbesb3f2BRWXlSvYn4MejYXDv4mZM2cqLS1NixcvVmhoqG36Lbfcov/8z/90anEAANjLMAytndxHxWWuPfRUVFrulpEgOM7hULNr1y699NJL1aa3bt1aubm5TikKAICGMAyDkZMmzOFvPwUEBKigoKDa9KNHj6pVq1ZOKaouL774ohITExUYGKjevXtr586dLl8nAABo/BwONSNGjNATTzyhsrIyST+l4pMnT2r27Nm67bbbnF7gz61evVozZ87U/PnztXfvXnXv3l2DBw9WXl6eS9cLAAAaP4dDzTPPPKNLly4pKipKxcXFuummm9ShQweFhobqqaeeckWNNs8++6wmTZqkCRMmqHPnzlqxYoWCg4O1cuXKGtuXlJSooKCgygMAAFiTwwcew8LClJ6erq1bt+rLL7/UpUuX1LNnT6WmprqiPpvS0lLt2bNHc+bMsU3z8fFRamqqMjMza1xm4cKFevzxx11aFwAArmKapstPfK5khevuNPhsqn79+qlfv37OrKVO586dU3l5uaKjo6tMj46O1uHDh2tcZs6cOZo5c6bteUFBgeLj411aJwAAzmCapm5fkak9bvqKuhWuu+NwqFm6dGmN0w3DUGBgoDp06KDf/OY38vX1veLirlRAQIACAgI8XQYAAA4rLit3W6CRrHHdHYcrf+655/Tdd9+pqKhI4eHhkqQLFy4oODhYISEhysvLU1JSkjZt2uTUUZHIyEj5+vrq7NmzVaafPXtWMTExTlsPAACNze65qQr2d81ggZWuu+PwicILFizQ9ddfr6+//lrnz5/X+fPndfToUfXu3VvPP/+8Tp48qZiYGM2YMcOphfr7+6tXr17KyMiwTauoqFBGRob69Onj1HUBANCYBPv7Kti/mYsenj+y4iwOj9TMnTtX69atU/v27W3TOnTooCVLlui2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VatWyTCMKo/AwEA3Vex8jz32WLXX06lTpzqXsdI+UCkxMbHadjAMQ1OnTq2xvRX2g88//1zDhw9XXFycDMPQ+++/X2W+aZqaN2+eYmNjFRQUpNTUVH399df19uvoZ4on1bUNysrKNHv2bHXt2lXNmzdXXFyc7r77bp05c6bOPhvynvKk+vaDtLS0aq9nyJAh9fbrTfuBVP92qOnzwTAMPf3007X26e59ocmHmtWrV2vmzJmaP3++9u7dq+7du2vw4MHKy8ursf0//vEPjR07Vvfee6/27dunkSNHauTIkTp48KCbK3eOLVu2aOrUqdq+fbvS09NVVlamQYMG6fLly3Uu16JFC+Xk5Nge2dnZbqrYNbp06VLl9WzdurXWtlbbByrt2rWryjZIT0+XJI0aNarWZbx9P7h8+bK6d++uF198scb5ixcv1tKlS7VixQrt2LFDzZs31+DBg/XDDz/U2qejnymeVtc2KCoq0t69e/Xoo49q7969evfdd3XkyBGNGDGi3n4deU95Wn37gSQNGTKkyut566236uzT2/YDqf7t8PPXn5OTo5UrV8owDN1222119uvWfcFs4m644QZz6tSptufl5eVmXFycuXDhwhrbjx492hw2bFiVab179zbvv/9+l9bpLnl5eaYkc8uWLbW2ee2118ywsDD3FeVi8+fPN7t37253e6vvA5V+//vfm+3btzcrKipqnG+1/UCS+d5779meV1RUmDExMebTTz9tm3bx4kUzICDAfOutt2rtx9HPlMbkl9ugJjt37jQlmdnZ2bW2cfQ91ZjUtA3Gjx9v3nrrrQ714837gWnaty/ceuut5sCBA+ts4+59oUmP1JSWlmrPnj1KTU21TfPx8VFqaqoyMzNrXCYzM7NKe0kaPHhwre29TX5+viQpIiKiznaXLl1SQkKC4uPjdeutt+pf//qXO8pzma+//lpxcXFKSkrSnXfeqZMnT9ba1ur7gPTTe+PNN9/UPffcU+fNX622H/xcVlaWcnNzq/yuw8LC1Lt371p/1w35TPE2+fn5MgxDLVu2rLOdI+8pb7B582ZFRUWpY8eOmjJlis6fP19r26awH5w9e1br16/XvffeW29bd+4LTTrUnDt3TuXl5YqOjq4yPTo6Wrm5uTUuk5ub61B7b1JRUaHp06frxhtv1LXXXltru44dO2rlypX64IMP9Oab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jFADwKO6d++uuXPnKjk5WXPmzFFgYKAiIyM1adIkJScna968eTp//ry+/PJLST+dx9KjRw8tWLBAnTp1Uo8ePbRy5Upt2rRJR48erXEd2dnZ8vX1VVRUVLV5t9xyi373u9+pQ4cOmj17tiIjI7Vp0yZJ0smTJ5WcnKx+/fopISFB/fr109ixY6ssHxcXp+zsbCdvFQANQagB4FHdunWz/ezr66urrrpKXbt2tU2Ljo6WJOXl5Un66YTfTZs22c7RCQkJUadOnSRJx48fr3EdxcXFCggIqPFk5p+vv/KQWeW60tLStH//fnXs2FEPPvigPv3002rLBwUF2UaRAHhWM08XAKBp8/Pzq/LcMIwq0yqDSEVFhSTp0qVLGj58uBYtWlStr9jY2BrXERkZqaKiIpWWlsrf37/e9Veuq2fPnsrKytLHH3+sjRs3avTo0UpNTdU777xja//999+rVatW9r5cAC5EqAHgVXr27Kl169YpMTFRzZrZ9xF23XXXSZIOHTpk+9leLVq00JgxYzRmzBjdfvvtGjJkiL7//ntFRERI+umk5R49ejjUJwDX4PATAK8ydepUff/99xo7dqx27dql48ePa8OGDZowYYLKy8trXKZVq1bq2bOntm7d6tC6nn32Wb311ls6fPiwjh49qrVr1yomJqbKdXa++OILDRo06EpeEgAnIdQA8CpxcXHatm2bysvLNWjQIHXt2lXTp09Xy5Yt5eNT+0faxIkT9be//c2hdYWGhmrx4sVKSUnR9ddfrxMnTuijjz6yrSczM1P5+fm6/fbbr+g1AXAOLr4HoEkoLi5Wx44dtXr1avXp08cpfY4ZM0bdu3fXI4884pT+AFwZRmoANAlBQUF644036rxInyNKS0vVtWtXzZgxwyn9AbhyjNQAAABLYKQGAABYAqEGAABYAqEGAABYAqEGAABYAqEGAABYAqEGAABYAqEGAABYAqEGAABYAqEGAABYwv8DNsDFo106Ks8AAAAASUVORK5CYII=",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import random\n",
+ "\n",
+ "random.seed('Some seed such that numbers generated are predictable')\n",
+ "parameters = {parameter_name: random.random() * 10 - 5 for parameter_name in all_epsilons}\n",
+ "\n",
+ "from qupulse.pulses.plotting import plot\n",
+ "_ = plot(gates[0], parameters)\n",
+ "_ = plot(gates[1], parameters)\n",
+ "_ = plot(sequences[1], parameters)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We now must construct the $S_k'$. For simplicity, we assume that there is only one initialization and one measurement pulse which are defined somehow and define only stubs here. We also define a waiting pulse with a variable length:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# stub for an initialization pulse of length 4\n",
+ "init = TablePT({0: [(0, 5), (4, 0, 'linear')]})\n",
+ "\n",
+ "# stub for a measurement pulse of length 12\n",
+ "measure = TablePT({0: [(0, 0), (12, 5, 'linear')]})\n",
+ "\n",
+ "# a wating pulse\n",
+ "wait = TablePT({0: [(0, 0), ('wait_duration', 0)]})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "For our example, let us assume that we want all $S_k'$ to take 200 ns (since we've chosen the $S_k$ to be rather short). We know that the duration of our gate pulses in nanoseconds is equal to the number of entries in the `TablePulseTemplate` objects (each voltage level is held for one unit of time in the tables which corresponds to $\\Delta t = 1$ ns by convention). Accordingly, the init pulse lasts for 4 ns and the measure pulse for 12 ns. The required length of the wait pulse can then be computed as follows:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[175, 178, 176]\n"
+ ]
+ }
+ ],
+ "source": [
+ "wait_times = []\n",
+ "desired_time = 200\n",
+ "\n",
+ "for m_k in m:\n",
+ " duration_k = 4 + 12 # init + measurement duration\n",
+ " for g_ki in m_k:\n",
+ " duration_k += len(gates[g_ki].entries) # add the number of entries of all gates in the sequence\n",
+ " wait_time_k = desired_time - duration_k\n",
+ " wait_times.append(wait_time_k)\n",
+ " \n",
+ "print(wait_times)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Finally we can construct the $S_k'$:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# an identity mapping for all epsilons\n",
+ "all_epsilons_map = {param_name: param_name for param_name in all_epsilons}\n",
+ "\n",
+ "gates_with_init_and_readout = [SequencePT((wait, {'wait_duration': wait_duration}),\n",
+ " init,\n",
+ " gate,\n",
+ " measure)\n",
+ " for gate, wait_duration in zip(sequences, wait_times)]\n",
+ "final_sequence = SequencePT(*gates_with_init_and_readout)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let us plot $S_1'$ to see whether we've accomplished our goal:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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1dChQ7Vq1SqrS/J6f/7zn2Wz2Zp89ezZ0/n+oUOHNGXKFLVv316RkZG6/PLLtW/fPgsr9i5LlizRJZdcotTUVNlsNn344YdN3jcMQw888IBSUlIUFhamUaNGafv27U22KS4u1tVXX63o6GjFxsbqhhtuUEVFhYnfhXc41VxOmjTpmJ/VsWPHNtmGuWwwe/ZsnXHGGYqKilJiYqIuu+wybd26tck2rvzZzs7O1rhx4xQeHq7ExET94Q9/UF1dnZnfildwZT7PO++8Y34+J0+e3GQb5rP12kyomTt3rqZPn64HH3xQa9asUf/+/TVmzBgVFBRYXZrX6927t/Ly8pxfS5cudb43bdo0/fe//9W7776rxYsXa+/evfr1r39tYbXe5eDBg+rfv7+ef/75474/Z84cPfPMM3rxxRe1cuVKRUREaMyYMTp06JBzm6uvvlo//vij5s+fr08++URLlizRzTffbNa34DVONZeSNHbs2CY/q2+//XaT95nLBosXL9aUKVO0YsUKzZ8/X7W1tRo9erQOHjzo3OZUf7br6+s1btw41dTU6Ntvv9Ubb7yh119/XQ888IAV35KlXJlPSbrpppua/HzOmTPH+R7z6SZGGzFkyBBjypQpzuf19fVGamqqMXv2bAur8n4PPvig0b9//+O+V1JSYgQHBxvvvvuu87XNmzcbkozly5ebVKHvkGR88MEHzucOh8NITk42HnvsMedrJSUlht1uN95++23DMAxj06ZNhiRj9erVzm0+//xzw2azGbm5uabV7m2OnkvDMIyJEycav/zlL0/4GebyxAoKCgxJxuLFiw3DcO3P9meffWYEBAQY+fn5zm1eeOEFIzo62qiurjb3G/AyR8+nYRjGueeea/z+978/4WeYT/doE0dqampq9P3332vUqFHO1wICAjRq1CgtX77cwsp8w/bt25WamqouXbro6quvVnZ2tiTp+++/V21tbZN57dmzpzp16sS8uiArK0v5+flN5i8mJkZDhw51zt/y5csVGxurwYMHO7cZNWqUAgICtHLlStNr9naLFi1SYmKievTooVtvvVVFRUXO95jLEystLZX0801/XfmzvXz5cvXt21dJSUnObcaMGaOysjL9+OOPJlbvfY6ez0ZvvfWW4uPj1adPH82cOVOVlZXO95hP92gTN7QsLCxUfX19kx8WSUpKStKWLVss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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "pl = plot(final_sequence.subtemplates[1], parameters)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let us also plot the whole sequence $S_1' | S_2' | S_3'$ as well"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "_ = plot(final_sequence, parameters)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Finally, we construct a single scanline which just repeats all three sequences over and over again. Since our $S_k'$ are short, we just build a scanline with a duration of 0.6 microseconds. With a duration for each $S_k'$ of 200 ns, we can fit 1'000 repetitions of $S_1' | S_2' | S_3'$ in our scanline. We will, however, not plot our scanline because it will be quite large despite it's short duration."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from qupulse.pulses import RepetitionPT\n",
+ "scanline = RepetitionPT(final_sequence, 1000)"
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 1
+}
diff --git a/doc/source/examples/03PointPulse.ipynb b/doc/source/examples/03PointPulse.ipynb
deleted file mode 100644
index 2eff02f84..000000000
--- a/doc/source/examples/03PointPulse.ipynb
+++ /dev/null
@@ -1,877 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# The PointPulseTemplate\n",
- "\n",
- "The `PointPulseTemplate`(or short `PointPT`) can be understood as a specialization of the `TablePulseTemplate`. It restricts the channels to all having the same time points in their entries and the same expression for their voltages.\n",
- "\n",
- "Let us first have a look at an simple example: "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{'A', 'B'}\n"
- ]
- }
- ],
- "source": [
- "from qupulse.pulses import PointPT\n",
- "\n",
- "point_template = PointPT([(0, 'v_0'),\n",
- " (1, 'v_1', 'linear'),\n",
- " ('t', 'v_0+v_1', 'jump')],\n",
- " channel_names=('A', 'B'))\n",
- "\n",
- "print(point_template.defined_channels)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "As you can see the pulse template has two channels although we only provided one expression for the voltage per time point. The value of this expression can either be scalar as we will see now for `v_0` or be a `numpy` array of the same length as the number of channels is like `v_1`. A value of the wrong length will result in an exception."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "%matplotlib notebook\n",
- "from qupulse.pulses.plotting import plot\n",
- "import numpy as np\n",
- "\n",
- "parameters = dict(t=3,\n",
- " v_0=1,\n",
- " v_1=np.array([1.2, 2.5]))\n",
- "\n",
- "_ = plot(point_template, parameters, sample_rate=100)"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python [default]",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.7.0"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/doc/source/examples/03SnakeChargeScan.ipynb b/doc/source/examples/03SnakeChargeScan.ipynb
new file mode 100644
index 000000000..eecebac43
--- /dev/null
+++ b/doc/source/examples/03SnakeChargeScan.ipynb
@@ -0,0 +1,364 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "fcefa3cf",
+ "metadata": {},
+ "source": [
+ "# Snake Charge Scan\n",
+ "\n",
+ "To manipulate an electron confined in a gate-defined quantum dot, it is essential to control the number of electrons i.e. the chemical potential of each quantum dot and the tunnel coupling among quantum dots. The charge stability diagram (CSD) shows changes in the electron occupation of the system in response to changes in applied gate voltages. This information can be used to extract the operation point for further experiments. For an infinitely slow gate sweep and in absence of charging effects the CSD shows the charge occupation of the ground state. In the real world however, the CSD can depend on the sweep direction of gate voltages and a charge state hysteresis in quantum dots has been observed and investigated. [1]\n",
+ "\n",
+ "[1] C. H. Yang, et al., Appl. Phys. Lett. 105, 183505 (2014)\n",
+ "\n",
+ "In this tutorial, a pulse for the bi-directional sweep of a CSD is constructed so that the hysteresis of charge occupancy in a double quantum dot system can be measured. For ease of analysis, 2 different measurement windows namingly `('x_neg', 'x_pos')` are defined providing the possibliy of inspecting two sweep direction individually. Options of `plot` function in `qupulse.pulses.plotting` will be explored as well."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "394631b6",
+ "metadata": {},
+ "source": [
+ "## Create a linear voltage sweep\n",
+ "\n",
+ "The basic building block of any such a diagram is a linear sweep of a channel. The first thing we need to do is write such a sweep. There are two options to implement this on a conceptional level.\n",
+ "\n",
+ "The first is to interpret the sweep as a continuous change of the voltage in time. The number of points here is just determined by the time resolution of our readout. Each readout point represents an average over a short time window of this continuous sweep.\n",
+ "\n",
+ "The second approach is to change the applied voltage in a fixed number of steps. Here the number of points is already a property of the pulse and each readout point is the average over the output at that point.\n",
+ "\n",
+ "In the following we will explore both variants and their hardware feasibility.\n",
+ "\n",
+ "### Continuous voltage sweep\n",
+ "\n",
+ "Let us first explore the first option. We can use a `TablePT` or a `PointPT` to implement it."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "7b6be5b4",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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b97Mhg0bAEhJSalzvP3795OYmIiHhwdPP/00s2bNwjAMUlJSeOKJJ5gyZUrznuBp7DxSwfIf91JUbgPgss6RjO/VAS93tSaJiIg0NZd28G4rmqqD91FLKXetXM+6A9UYQISvJ7enJdIrMsBp7yEiItKStfsO3nJqxypsjF2+kX2HqwEYnhTGDefG4ethdnFlIiIi7YvCUgsV5ONBSoQfh0squblPB85PjHJ1SSIiIu2SwlILZTKZ+Nvorqz/voKYMF12ExERcRX1EG7Bgn098PfUP5GIiIgr6ZtYREREpB4KSyIiIiL1UFgSERERqYfCkoiIiEg9FJZERERE6qGwJCIiIlIPhSURERGReigsiYiIiNRDYUlERESkHgpLIiIiIvVQWBIRERGph8KSiIiISD0UlkRERETqobAkIiIiUg+FJREREZF6KCyJiIiI1ENhSURERKQeCksiIiIi9VBYEhEREamHwpKIiIhIPRSWREREROqhsCQiIiJSD4UlERERkXooLImIiIjUQ2FJREREpB4KSyIiIiL1UFgSERERqYe7qwtoCwzDAMBisTj1uBUVFZSXl1NTc4Ty8lKnHltERKQ1sNlsWK3lWCwWampqnHrsX7+3f/0ePxWFJScoLT0eZOLi4lxciYiIiDRWaWkpQUFBp1xvMk4Xp+S0HA4H+fn5BAQEYDKZnHZci8VCXFwcubm5BAYGOu24Upc+5+ajz7p56HNuHvqcm0dTfs6GYVBaWkpsbCxubqfumaSWJSdwc3OjY8eOTXb8wMBA/UdsBvqcm48+6+ahz7l56HNuHk31OdfXovQrdfAWERERqYfCkoiIiEg9FJZaMC8vLx544AG8vLxcXUqbps+5+eizbh76nJuHPufm0RI+Z3XwFhEREamHWpZERERE6qGwJCIiIlIPhSURERGReigsiYiIiNRDYakFe/rpp0lMTMTb25uBAweSnp7u6pLalAULFjBgwAACAgKIjIzkiiuuIDMz09VltXl///vfMZlM3HHHHa4upc3Jy8tj/PjxhIWF4ePjQ69evdi4caOry2pT7HY79913H0lJSfj4+NCpUycefvjh084tJqf39ddfc/nllxMbG4vJZOJf//pXnfWGYXD//fcTExODj48Pw4cP55dffmmW2hSWWqi33nqL2bNn88ADD7B582Z69+7NyJEjOXjwoKtLazO+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D29vb8LDw5kyZQqdO3fm/vvv58iRI2zbtg043k+oT58+zJ8/n27dutGnTx9efPFFvvjiC3bv3n3S98jOzsZsNhMZGXnCuksvvZTbbruNlJQU7r77bsLDw/niiy8AyMnJoXPnzlxwwQUkJCRwwQUX8Kc//anO/rGxsWRnZzv5UxGRM6GwJCJt0jnnnFP73Gw2ExYWRq9evWqXRUVFAXDw4EHgeEftL774orYPlL+/P926dQNg7969J32PyspKvLy8TtoJ/bfv/+ulw1/fa9KkSWzdupWuXbsyY8YMPv300xP29/HxqW31EhHXcnd1ASIiTcHDw6POa5PJVGfZrwHH4XAAUFZWxuWXX87ChQtPOFZMTMxJ3yM8PJyKigpsNhuenp6nff9f36tv377s37+fjz/+mM8++4yrr76a4cOH884779RuX1xcTERERENPV0SakMKSiAjHA8y7775LYmIi7u4N+9V47rnnArBjx47a5w0VGBjINddcwzXXXMNVV13FqFGjKC4uJjQ0FDje2bxPnz6NOqaINA1dhhMRAaZNm0ZxcTF/+tOfyMjIYO/evXzyySfccMMN2O32k+4TERFB3759+fbbbxv1Xk888QRvvPEGu3btYvfu3axatYro6Og640R98803jBgx4mxOSUScRGFJRITjHaq/++477HY7I0aMoFevXtxxxx0EBwfj5nbqX5U33XQTr732WqPeKyAggEcffZT+/fszYMAAsrKy+Pe//137PuvXr6ekpISrrrrqrM5JRJxDg1KKiJyFyspKunbtyltvvcWgQYOccsxrrrmG3r17c8899zjleCJydtSyJCJyFnx8fFi5cmW9g1c2hs1mo1evXsyaNcspxxORs6eWJREREZF6qGVJREREpB4KSyIiIiL1UFgSERERqYfCkoiIiEg9FJZERERE6qGwJCIiIlIPhSURERGReigsiYiIiNRDYUlERESkHv8PbA3VKrVwamUAAAAASUVORK5CYII=",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "\n",
+ "from qupulse.pulses import *\n",
+ "from qupulse.plotting import *\n",
+ "\n",
+ "linear_cont = TablePT({'X': [(0, 'x_start'),\n",
+ " ('tx_sweep', 'x_stop', 'linear')]},\n",
+ " measurements=[('M', 0, 'tx_sweep')])\n",
+ "\n",
+ "x_sweep_params = {'x_start': -3.3, 'x_stop': -1.5, 'tx_sweep': 10.}\n",
+ "\n",
+ "_ = plot(linear_cont, parameters=x_sweep_params, stepped=False, plot_measurements={'M'})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "dd2a62bd",
+ "metadata": {},
+ "source": [
+ "You'll notice that this pulse does only have one measurement window over the whole range attached. For a sweep with `n` points we would maybe expect `n` measurement windows. However, measurement windows require a pulse template to be attached to and there is no way in qupulse (yet in 2023) to change or parameterize the number of measurement windows directly. You have to change the number of pulse template instantiations f.i. with a `RepetitionPT` or a `ForLoopPT` to change the number of measurement windows.\n",
+ "\n",
+ "We can however interpret the measurement window as the time window where the sweep happens and use it as such in our measurement configuration and data analysis. qupulse does not enforce or promote a particular meaning of measurement windows. qupulse simply tracks them through complex pulse trees and gives you their final position in an instantiated pulse.\n",
+ "\n",
+ "### Stepped sweep\n",
+ "\n",
+ "Next we will explore how to write a \"stepped\" sweep. We want the number of steps to be parameterized by `n_x` and the range by `x_start` and `x_stop`. The total duration of the sweep shall be `tx_sweep`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "fcdfe86c",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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5QEAABomS69ZmjBhgtLS0kzHJCUleZ5HRUUpKipK5513njp06KD4+Hht3rxZPXv2POP2x44dU2pqqsLDw7V69eqznrbr3r27ZsyYIafTKYfDUa3jAQAADY+lZSk6OlrR0dE12tbtdktSlQut/1txcbEGDhwoh8OhNWvWKCQk5Kz7zcrKUrNmzShKAABAkp98G27Lli3atm2bLr/8cjVr1ky7d+/W1KlT1bZtW8+sUl5envr166fly5erW7duKi4u1oABA1RaWqoXXnhBxcXFnm+tRUdHy263680339TBgwfVo0cPhYSEKDMzU7NmzdJ9991n5eECAIB6xC/KUlhYmF5//XU9/PDDKikpUYsWLZSamqopU6Z4ZoAqKiqUnZ2t0tJSSdIXX3yhLVu2SJKSk5Or7G/Pnj1KTExUUFCQFixYoPT0dBmGoeTkZM2bN0+jR4+u2wMEAAD1ll+UpU6dOmn9+vWmYxITE6tc7N27d++zXvydmpqq1NRUn2T0NcMwVFruktNlKKjSLVuly+pIZ1VW6bY6AgAAPucXZem36ESFS10f++g/r7ItzQIAwG+ZX91nCf4h1l6mYLvN6hgAAPgEM0v1VGiQXZ9P+r0++/QLNW78O4WGhlodySuFhUe1fevHstk6Wh0FAACfoCzVUzabTWHBdjnsNoUEBigk0G51JK847AGyMakEAGhAOA0HAABggrIEAABggrIEAABggrIEAABggrIEAABggrIEAABggrIEAABggrIEAABggrIEAABggrIEAABggrIEAABggrIEAABggrIEAABggrIEAABggrIEAABggrIEAABggrIEAABggrIEAABggrIEAABggrIEAABggrIEAABggrIEAABggrIEAABggrIEAABggrIEAABggrIEAABggrIEAABgItDqAA2BYRiSpOLiYp/ut7S0VCUlJaqs/EklJcd8uu/aUnysSGVlJ/Tzzz+psrLc6jheIXPd8cfcZK4bZK4b/pi5vLxcTmeJiouLVVlZ6dN9n/y7ffLv+JlQlnzg2LFfikx8fLzFSQAAQHUdO3ZMERERZ1xvM85Wp3BWbrdb+fn5Cg8Pl81m89l+i4uLFR8fr/3796tJkyY+2y+q4nOuO3zWdYPPuW7wOdeN2vycDcPQsWPH1LJlSwUEnPnKJGaWfCAgIECtWrWqtf03adKE/yHWAT7nusNnXTf4nOsGn3PdqK3P2WxG6SQu8AYAADBBWQIAADBBWarHHA6HHn74YTkcDqujNGh8znWHz7pu8DnXDT7nulEfPmcu8AYAADDBzBIAAIAJyhIAAIAJyhIAAIAJyhIAAIAJylI9tmDBAiUmJiokJETdu3fX1q1brY7UoGRkZOjSSy9VeHi4YmJidM011yg7O9vqWA3eY489JpvNpnvvvdfqKA1OXl6ehg0bpubNmys0NFSdOnXS559/bnWsBsXlcmnq1Klq06aNQkND1bZtW82YMeOsvy2Gs/voo4909dVXq2XLlrLZbPrXv/5VZb1hGJo2bZpatGih0NBQ9e/fXz/88EOdZKMs1VOvvPKKxo8fr4cfflhffPGFLrroIg0cOFCHDh2yOlqDsXHjRo0dO1abN29WZmamKioqNGDAAJWUlFgdrcHatm2bnn32WXXu3NnqKA3Ozz//rMsuu0xBQUF69913tXPnTj3xxBNq1qyZ1dEalNmzZ2vhwoX6+9//ru+++06zZ8/W448/rvnz51sdze+VlJTooosu0oIFC067/vHHH9fTTz+tZ555Rlu2bFGjRo00cOBAlZWV1X44A/VSt27djLFjx3peu1wuo2XLlkZGRoaFqRq2Q4cOGZKMjRs3Wh2lQTp27JjRrl07IzMz07jiiiuMe+65x+pIDcrEiRONyy+/3OoYDd6gQYOMW2+9tcqya6+91rj55pstStQwSTJWr17tee12u424uDhjzpw5nmWFhYWGw+EwXnrppVrPw8xSPVReXq7t27erf//+nmUBAQHq37+/Nm3aZGGyhq2oqEiSFBkZaXGShmns2LEaNGhQlX+v4Ttr1qxR165dNXToUMXExCglJUX//Oc/rY7V4PTq1Uvr1q3T999/L0n66quv9Mknn+jKK6+0OFnDtmfPHhUUFFT570dERIS6d+9eJ38X+SHdeujIkSNyuVyKjY2tsjw2Nla7du2yKFXD5na7de+99+qyyy7ThRdeaHWcBufll1/WF198oW3btlkdpcH68ccftXDhQo0fP14PPvigtm3bpnHjxik4OFgjR460Ol6DMWnSJBUXF6t9+/ay2+1yuVyaOXOmbr75ZqujNWgFBQWSdNq/iyfX1SbKEqBfZj127NihTz75xOooDc7+/ft1zz33KDMzUyEhIVbHabDcbre6du2qWbNmSZJSUlK0Y8cOPfPMM5QlH3r11Ve1YsUKvfjii7rggguUlZWle++9Vy1btuRzbsA4DVcPRUVFyW636+DBg1WWHzx4UHFxcRalarjuvvtuvfXWW9qwYYNatWpldZwGZ/v27Tp06JAuueQSBQYGKjAwUBs3btTTTz+twMBAuVwuqyM2CC1atFDHjh2rLOvQoYNyc3MtStQw3X///Zo0aZJuvPFGderUScOHD1d6eroyMjKsjtagnfzbZ9XfRcpSPRQcHKwuXbpo3bp1nmVut1vr1q1Tz549LUzWsBiGobvvvlurV6/W+vXr1aZNG6sjNUj9+vXTN998o6ysLM+ja9euuvnmm5WVlSW73W51xAbhsssuO+XWF99//71at25tUaKGqbS0VAEBVf902u12ud1uixL9NrRp00ZxcXFV/i4WFxdry5YtdfJ3kdNw9dT48eM1cuRIde3aVd26ddPf/vY3lZSU6JZbbrE6WoMxduxYvfjii3rjjTcUHh7uOe8dERGh0NBQi9M1HOHh4adcB9aoUSM1b96c68N8KD09Xb169dKsWbN0/fXXa+vWrVq0aJEWLVpkdbQG5eqrr9bMmTOVkJCgCy64QF9++aXmzZunW2+91epofu/48ePKycnxvN6zZ4+ysrIUGRmphIQE3XvvvfrrX/+qdu3aqU2bNpo6dapatmypa665pvbD1fr37VBj8+fPNxISEozg4GCjW7duxubNm62O1KBIOu1j6dKlVkdr8Lh1QO148803jQsvvNBwOBxG+/btjUWLFlkdqcEpLi427rnnHiMhIcEICQkxkpKSjIceeshwOp1WR/N7GzZsOO1/k0eOHGkYxi+3D5g6daoRGxtrOBwOo1+/fkZ2dnadZLMZBrcdBQAAOBOuWQIAADBBWQIAADBBWQIAADBBWQIAADBBWQIAADBBWQIAADBBWQIAADBBWQIAADBBWQLg99LS0urmJw/OYPjw4Zo1a5ZP9lVeXq7ExER9/vnnPtkfgHPHHbwB1Gs2m810/cMPP6z09HQZhqGmTZvWTahf+eqrr9S3b1/t27dPjRs39sk+//73v2v16tVVfjQUgHUoSwDqtZM/cCxJr7zyiqZNm6bs7GzPssaNG/uspNTEbbfdpsDAQD3zzDM+2+fPP/+suLg4ffHFF7rgggt8tl8ANcNpOAD1WlxcnOcREREhm81WZVnjxo1POQ3Xu3dv/eUvf9G9996rZs2aKTY2Vv/85z9VUlKiW265ReHh4UpOTta7775b5b127NihK6+8Uo0bN1ZsbKyGDx+uI0eOnDGby+XSa6+9pquvvrrK8sTERM2aNUu33nqrwsPDlZCQoEWLFnnWl5eX6+6771aLFi0UEhKi1q1bKyMjw7O+WbNmuuyyy/Tyyy+f46cHwBcoSwAapOeee05RUVHaunWr/vKXv2jMmDEaOnSoevXqpS+++EIDBgzQ8OHDVVpaKkkqLCxU3759lZKSos8//1zvvfeeDh48qOuvv/6M7/H111+rqKhIXbt2PWXdE088oa5du+rLL7/UXXfdpTFjxnhmxJ5++mmtWbNGr776qrKzs7VixQolJiZW2b5bt276+OOPffeBAKgxyhKABumiiy7SlClT1K5dO02ePFkhISGKiorS6NGj1a5dO02bNk0//fSTvv76a0m/XCeUkpKiWbNmqX379kpJSdGSJUu0YcMGff/996d9j3379slutysmJuaUdVdddZXuuusuJScna+LEiYqKitKGDRskSbm5uWrXrp0uv/xytW7dWpdffrluuummKtu3bNlS+/bt8/GnAqAmKEsAGqTOnTt7ntvtdjVv3lydOnXyLIuNjZUkHTp0SNIvF2pv2LDBcw1U48aN1b59e0nS7t27T/seJ06ckMPhOO1F6L9+/5OnDk++V1pamrKysnT++edr3LhxWrt27Snbh4aGema9AFgr0OoAAFAbgoKCqry22WxVlp0sOG63W5J0/PhxXX311Zo9e/Yp+2rRosVp3yMqKkqlpaUqLy9XcHDwWd//5Htdcskl2rNnj95991198MEHuv7669W/f3+99tprnvFHjx5VdHS0t4cLoBZRlgBAvxSYVatWKTExUYGB3v2n8eKLL5Yk7dy50/PcW02aNNENN9ygG264Qdddd51SU1N19OhRRUZGSvrlYvOUlJRq7RNA7eA0HABIGjt2rI4ePaqbbrpJ27Zt0+7du/X+++/rlltukcvlOu020dHRuuSSS/TJJ59U673mzZunl156Sbt27dL333+vlStXKi4ursp9oj7++GMNGDDgXA4JgI9QlgBAv1xQ/emnn8rlcmnAgAHq1KmT7r33XjVt2lQBAWf+T+Vtt92mFStWVOu9wsPD9fjjj6tr16669NJLtXfvXr3zzjue99m0aZOKiop03XXXndMxAfANbkoJAOfgxIkTOv/88/XKK6+oZ8+ePtnnDTfcoIsuukgPPvigT/YH4NwwswQA5yA0NFTLly83vXlldZSXl6tTp05KT0/3yf4AnDtmlgAAAEwwswQAAGCCsgQAAGCCsgQAAGCCsgQAAGCCsgQAAGCCsgQAAGCCsgQAAGCCsgQAAGCCsgQAAGDi/wPKyEPf3d8jVAAAAABJRU5ErkJggg==",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "step = ConstantPT(duration='tx_sweep / n_x', amplitude_dict={'X': 'x_start + x_i * (x_stop - x_start) / (n_x - 1)'},\n",
+ " measurements=[('M', 0, 'tx_sweep / n_x')])\n",
+ "\n",
+ "# equivalent to ForLoopPT(step, 'x_i', 'n_x')\n",
+ "linear_step = step.with_iteration('x_i', 'n_x')\n",
+ "\n",
+ "x_sweep_params.update(n_x=10)\n",
+ "\n",
+ "_ = plot(linear_step, parameters=x_sweep_params, plot_measurements={'M'})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "205f94ed",
+ "metadata": {},
+ "source": [
+ "Here we see that each step has its own measurement window.\n",
+ "\n",
+ "## The snake: going forward and backward\n",
+ "\n",
+ "We can now utilize the `TimeReversalPT` to go backward and combine both direction with a `SequencePT`. Furthermore, we want to rename the measurement windows to discriminate between forward and backward measurements. We can utilize the `with_*` methods and the overloaded matrix multiplication operator `@` to make this very concise."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "8ac1f53d",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "image/png": "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sQT0iOgKdzc32WoX6o3o9tkl2epR7SQ5PNWrV890nZOfnx+uXr2KJ598EgBw7949SbWmTZuG6OjoCscEBweb/k5LS0PPnj0RERGBNWvWSGscQKdOnQA8OHLVuHFj+Pr64vTp02Zjbt++DQDlXicFwHSLBrJNaqUAjUr+B1erlQoIPOhEZBMEQYC9IMJBpYCjHR9kT9JIDk+dO3fGsWPH0Lx5cwwYMADTpk3Dzz//jB07dqBz586Sanl5ecHLy8uisampqejZsyfat2+P9evXQ6GQfMYRiYmJAB6EPgAIDw/HokWLcOfOHXh7ewMA4uPj4eLighYtePSAiIiISpOcQJYvX246grNgwQL07t0bX3zxBYKCgkw3yZRbamoqevTogYCAACxbtgx3795FRkaG2XVJqampCA0NNR1Junr1KmJjY3H27Flcv34du3btwogRI9CtWze0atUKANC3b1+0aNECw4cPx08//YS9e/di9uzZmDRpEo8sERERUZkkH3l6+DRavXr18PHHH8vaUFni4+ORnJyM5ORkNGzY0Gye+N9fM+h0OiQlJaGgoAAAYG9vj/379+Mf//gH8vPz4e/vj6ioKMyePdu0rFKpxDfffIMJEyYgPDwc9erVw8iRI83uC0VERET0sCqFpzNnzqB+/fpm07OystCuXTv8/vvvsjVXIjo6utJro4KCgkxBCgD8/f1x5MiRSmsHBgZiz549j9oiERERPSYkn7a7fv06DIbSNxTTarVITU2VpSkiIiKi2sriI0+7du0y/b137164urqa3hsMBhw4cABBQUGyNkdERERU21gcngYPHgzgwc87R44caTbPzs4OQUFBeP/992VtjoiIiKi2sTg8GY0Pnt3VqFEjnDlzBp6enlZrioiIiKi2knzB+LVr16zRBxEREZFNsCg8ffTRRxYXnDJlSpWbISIiIqrtLApPH3zwgUXFBEFgeCIiIqI6zaLwxFN1RERERA9If0DcQ0RRNLsxJREREVFdV6XwtGnTJoSFhcHBwQEODg5o1aoVPvnkE7l7IyIiIqp1JP/abvny5ZgzZw4mT56Mp59+GgBw7NgxjB8/Hvfu3UNMTIzsTRIRERHVFpLD04oVK7Bq1SqMGDHCNG3QoEF48sknMX/+fIYnIiIiqtMkn7ZLT09HREREqekRERFIT0+XpSkiIiKi2kpyeAoJCcHW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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "snake_1d_cont = linear_cont.with_mapping({'M': 'x_fwd'}) @ linear_cont.with_time_reversal().with_mapping({'M': 'x_bwd'})\n",
+ "snake_1d_step = linear_step.with_mapping({'M': 'x_fwd'}) @ linear_step.with_time_reversal().with_mapping({'M': 'x_bwd'})\n",
+ "\n",
+ "_ = plot(snake_1d_cont, parameters=x_sweep_params, plot_measurements={'x_fwd', 'x_bwd'}, stepped=False)\n",
+ "_ = plot(snake_1d_step, parameters=x_sweep_params, plot_measurements={'x_fwd', 'x_bwd'})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "872a0324",
+ "metadata": {},
+ "source": [
+ "## Two-dimensional sweep\n",
+ "\n",
+ "The next step is to make two-dimensional scans. We need to add the `Y` channel and loop over the desired voltages. We can use the `ParallelChannelPT` and `ForLoopPT` for that. The number of points in y-direction shall be `n_y`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "c66d64e0",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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cRAgIXlqeGwi8tTUsUCz+WvNe9PSeXq/N80d21j+2vLepFTZrKigBj9OpzPq77G1OlTQE/6lTbn4vCSnRIiOzel0kpeXl+xkxCRcbwwaNCjJKRFCtLXuULR0+srVRqqqsn79eoYNG8aOHTvIzc1NdpJ0V1VVRa9evSR/aSiT8wZ1+VuzZg2DBw/GYEiP3nVVVdm9ezeaptG7d++MfH86StmT/KWfcN62b9+Ooih07969TXVHh2rpMRgM9OjRA4Dc3NyMKxz1Sf7SVybnDaBHjx5pE/BAqN7o2bMnVVVVQGa/P5mcN5D8pbO8vDxd8pY+NY8QQgghRBtI0COEEEKIDqHDBT1Wq5W77roLq9Wa7KQkhOQvfWVy3iD985fu6W9OJucNJH/pTO+8daiBzEIIIYTouDpcS48QQgghOiYJeoQQQgjRIUjQI4QQQogOQYIeIYQQQnQIHS7oeeqpp+jbty82m43x48ezdOnSZCcpbvPnz+eII44gJyeHzp07c84557B+/fqoYzweD9dccw1FRUVkZ2dz7rnnsm/fviSluPXuv/9+FEXhxhtvjOxL97zt2rWLiy66iKKiIux2OyNHjmT58uWR1zVN484776Rbt27Y7XZOPvlkNm7cmMQUxy4YDHLHHXfQr18/7HY7/fv35957741aLycd85cJ9QZI3ZHueZO6Q4f8aR3I66+/rlksFu3FF1/UfvzxR+2KK67Q8vPztX379iU7aXH52c9+pr300kva6tWrtZUrV2pnnHGG1rt3b626ujpyzFVXXaX16tVL+/TTT7Xly5drRx11lDZhwoQkpjp+S5cu1fr27asddthh2g033BDZn855Kysr0/r06aNNmzZN++abb7QtW7Zo//3vf7VNmzZFjrn//vu1vLw87d1339W+//577ayzztL69eunud3uJKY8NvPmzdOKioq0Dz74QNu6dav2z3/+U8vOztb+9Kc/RY5Jt/xlSr2haVJ3pHPepO7QJ38dKug58sgjtWuuuSbyczAY1Lp3767Nnz8/ialqu9LSUg3QFi5cqGmaplVUVGhms1n75z//GTlm7dq1GqB9/fXXyUpmXJxOpzZw4EDtk08+0Y477rhIxZXuebv11lu1SZMmNfm6qqpa165dtYceeiiyr6KiQrNardo//vGP9khim5x55pnapZdeGrXvV7/6lfbb3/5W07T0zF+m1huaJnVHOuVN6g598tdhurd8Ph8rVqzg5JNPjuwzGAycfPLJfP3110lMWdtVVlYCUFhYCMCKFSvw+/1ReR0yZAi9e/dOm7xec801nHnmmVF5gPTP23vvvce4ceOYPHkynTt3ZsyYMbzwwguR17du3crevXuj8peXl8f48ePTIn8TJkzg008/ZcOGDQB8//33LF68mNNPPx1Iv/xlcr0BUnekU96k7tAnfx1mwdEDBw4QDAbp0qVL1P4uXbqwbt26JKWq7VRV5cYbb2TixImMGDECgL1792KxWMjPz486tkuXLuzduzcJqYzP66+/zrfffsuyZcsOeS3d87ZlyxaeeeYZZs6cydy5c1m2bBnXX389FouFqVOnRvLQWDlNh/zNnj2bqqoqhgwZgtFoJBgMMm/ePH77298CpF3+MrXeAKk7wtIlb1J36JO/DhP0ZKprrrmG1atXs3jx4mQnRRc7duzghhtu4JNPPsFmsyU7ObpTVZVx48Zx3333ATBmzBhWr17Ns88+y9SpU5OcurZ78803ee211/j73//O8OHDWblyJTfeeCPdu3fPiPxlEqk70ovUHfroMN1bxcXFGI3GQ0bq79u3j65duyYpVW1z7bXX8sEHH7BgwQJ69uwZ2d+1a1d8Ph8VFRVRx6dDXlesWEFpaSmHH344JpMJk8nEwoULefzxxzGZTHTp0iVt8wbQrVs3hg0bFrVv6NChbN++HSCSh3QtpzfffDOzZ8/mwgsvZOTIkUyZMoUZM2Ywf/58IP3yl4n1BkjdUV865A2k7tArfx0m6LFYLIwdO5ZPP/00sk9VVT799FOOPvroJKYsfpqmce211/LOO+/w2Wef0a9fv6jXx44di9lsjsrr+vXr2b59e8rn9aSTTuKHH35g5cqVkX/jxo3jt7/9bWQ7XfMGMHHixEMeEd6wYQN9+vQBoF+/fnTt2jUqf1VVVXzzzTdpkT+Xy4XBEF2tGI1GVFUF0i9/mVRvgNQd6Zo3kLpDt/zpMeo6Xbz++uua1WrVXn75ZW3NmjXa7373Oy0/P1/bu3dvspMWl+nTp2t5eXna559/ru3Zsyfyz+VyRY656qqrtN69e2ufffaZtnz5cu3oo4/Wjj766CSmuvXqP4Ghaemdt6VLl2omk0mbN2+etnHjRu21117THA6H9re//S1yzP3336/l5+dr//73v7VVq1ZpZ599dto8djp16lStR48ekcdO3377ba24uFi75ZZbIsekW/4ypd7QNKk70jlvUnfok78OFfRomqY98cQTWu/evTWLxaIdeeSR2pIlS5KdpLgBjf576aWXIse43W7t6quv1goKCjSHw6H98pe/1Pbs2ZO8RLdBw4or3fP2/vvvayNGjNCsVqs2ZMgQ7fnnn496XVVV7Y477tC6dOmiWa1W7aSTTtLWr1+fpNTGp6qqSrvhhhu03r17azabTSspKdFuu+02zev1Ro5Jx/xlQr2haVJ3pHvepO5oe/4UTas33aEQQgghRIbqMGN6hBBCCNGxSdAjhBBCiA5Bgh4hhBBCdAgS9AghhBCiQ5CgRwghhBAdggQ9QgghhOgQJOgRQgghRIcgQY8QQgghOgQJeoTupk2bxjnnnJO0+0+ZMiWyEnFb+Xw++vbty/Lly3W5nhCicVJviPYgMzKLuCiK0uzrd911FzNmzEDTNPLz89snUfV8//33nHjiifz0009kZ2frcs0nn3ySd955J2qhOyFE7KTeEKlCgh4Rl71790a233jjDe68886olX+zs7N1qzRa4/LLL8dkMvHss8/qds3y8nK6du3Kt99+y/Dhw3W7rhAdhdQbUm+kCuneEnHp2rVr5F9eXh6KokTty87OPqSZ+vjjj+e6667jxhtvpKCggC5duvDCCy9QU1PDJZdcQk5ODgMGDOA///lP1L1Wr17N6aefTnZ2Nl26dGHKlCkcOHCgybQFg0H+9a9/8Ytf/CJqf9++fbnvvvu49NJLycnJoXfv3jz//POR130+H9deey3dunXDZrPRp08f5s+fH3m9oKCAiRMn8vrrr7fxtydExyT1hkgVEvSIdvHXv/6V4uJili5dynXXXcf06dOZPHkyEyZM4Ntvv+XUU09lypQpuFwuACoqKjjxxBMZM2YMy5cv56OPPmLfvn2cf/75Td5j1apVVFZWMm7cuENee+SRRxg3bhzfffcdV199NdOnT49803z88cd57733ePPNN1m/fj2vvfYaffv2jTr/yCOPZNGiRfr9QoQQLZJ6Q+hOr2XhRcfz0ksvaXl5eYfsnzp1qnb22WdHfj7uuOO0SZMmRX4OBAJaVlaWNmXKlMi+PXv2aID29ddfa5qmaffee6926qmnRl13x44dGqCtX7++0fS88847mtFo1FRVjdrfp08f7aKLLor8rKqq1rlzZ+2ZZ57RNE3TrrvuOu3EE0885Lz6/vSnP2l9+/Zt8nUhRGyk3hDJJC09ol0cdthhkW2j0UhRUREjR46M7OvSpQsApaWlQGhg4YIFCyJ9/dnZ2QwZMgSAzZs3N3oPt9uN1WptdNBk/fuHm9bD95o2bRorV65k8P+3c8csyYVhGMcvowIHQ0ShnNycRD3k0B6Hljbx0Bi0BZGr3yAHP4QgEdVaJG4aDe0RgegnOCCIwoETDS8JUu+r0qlXev6/ScVz3z5wuLl8lCed1snJiZrN5ofrw+Hw5NskgJ/B3EDQVv/3B4AZ1tbWpp6HQqGp194HzuvrqyRpOBxqf39fZ2dnH2ptbW192iMej2s0GsnzPK2vr8/s/97Lsiz1ej3d3t6q1WqpVCppd3dXV1dXk/e7rqtEIjHvcgEEgLmBoBF6sJQsy9L19bVSqZRWV+e7TXO5nCTp6elp8nheGxsbchxHjuOoWCxqb29PrusqFotJ+vPnyHw+v1BNAD+LuYFZ+HkLS+n4+Fiu6+rg4ECPj4/qdru6u7vT4eGhfN//9JpEIiHLstTpdBbqVavVdH5+rufnZ728vOjy8lKbm5tT54W0223Ztv2VJQH4ZswNzELowVJKJpO6v7+X7/uybVuZTEanp6eKRqNaWfn7bXt0dKRGo7FQr0gkomq1qu3tbRUKBfX7fd3c3Ez6PDw8aDAYqFgsfmlNAL4XcwOzcDghfpXxeKx0Oq2Liwvt7OwEUtNxHGWzWVUqlUDqAVguzA1zsNODXyUcDqter//zMLJFeJ6nTCajcrkcSD0Ay4e5YQ52egAAgBHY6QEAAEYg9AAAACMQegAAgBEIPQAAwAiEHgAAYARCDwAAMAKhBwAAGIHQAwAAjEDoAQAARngDkv7gHhtWIaEAAAAASUVORK5CYII=",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "y_value = {'Y': 'y_start + y_i * (y_stop - y_start) / (n_y - 1)'}\n",
+ "\n",
+ "# with_* methods\n",
+ "snake_step = snake_1d_step\\\n",
+ " .with_parallel_channels(y_value)\\\n",
+ " .with_iteration('y_i', 'n_y')\n",
+ "\n",
+ "# Direct class instantiation\n",
+ "snake_linear = ForLoopPT(ParallelChannelPT(snake_1d_cont, y_value), 'y_i', 'n_y')\n",
+ "\n",
+ "sweep_params_2d = {**x_sweep_params, 'y_start': -1.1, 'y_stop': 0.6, 'n_y': 4}\n",
+ "\n",
+ "_, (ax1, ax2) = plt.subplots(1, 2, sharey=True)\n",
+ "_ = plot(snake_linear, parameters=sweep_params_2d, plot_measurements={'x_fwd', 'x_bwd'}, stepped=False, axes=ax1)\n",
+ "_ = plot(snake_step, parameters=sweep_params_2d, plot_measurements={'x_fwd', 'x_bwd'}, axes=ax2)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "157f6ece",
+ "metadata": {},
+ "source": [
+ "Now that we created a two-dimensional scan we can use the `plot_2d` function to inspect what it does in voltage space i.e. without a time axis and compare it to a regular \"forward only\" charge scan. First we create that one from the `linear_step` sweep defined above."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "8c7802fb-bff3-4c4e-80dc-d9be15f217ca",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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lzIyMgJ2xhUd84Vs27ZNkgJ2zr/kdDpVVFRkyvmW5dwxX0igz/eaa67Rjh07tG3bNtejc+fOuu2221xf+2TOvr0nOrB88MEHht1uN+bNm2d8//33xu9//3sjLi7OOHr0qL9bqxYTJ040VqxYYezbt89Ys2aN0bt3byM+Pt44fvy4v1vzmdzcXGPr1q3G1q1bDUnGnDlzjK1btxoHDhwwDMMwnnrqKSMuLs745JNPjO3btxvXX3+90bRpU6OgoMDPnVdOecebm5trPPjgg8batWuNffv2Gd98843RsWNHo3nz5kZhYaG/W6+UMWPGGLGxscaKFSuMI0eOuB75+fmumtGjRxuNGzc2li1bZmzatMno3r270b17dz92XTUVHfOePXuMJ554wti0aZOxb98+45NPPjFSU1ONK6+80s+dV87kyZONlStXGvv27TO2b99uTJ482bBYLMbXX39tGIb55msY5R+z2eZbll++UswXcw7qgGMYhvHSSy8ZjRs3NsLCwowuXboY69at83dL1WbIkCFGcnKyERYWZlx88cXGkCFDjD179vi7LZ9avny5Iem8x7BhwwzDOPtS8UcffdRITEw07Ha7cc011xi7du3yb9NVUN7x5ufnG9dee62RkJBghIaGGikpKcbIkSMDOsBf6FglGW+99ZarpqCgwLjnnnuMunXrGpGRkcaNN95oHDlyxH9NV1FFx5yRkWFceeWVRr169Qy73W6kpaUZDz30kJGdne3fxivprrvuMlJSUoywsDAjISHBuOaaa1zhxjDMN1/DKP+YzTbfsvwy4PhizhbDMIwqnGkCAACodYL2HhwAAGBeBBwAAGA6BBwAAGA6BBwAAGA6BBwAAGA6BBwAAGA6BBwAAGA6BBwAAGA6BBwANWb48OG64YYb/Lb/O+64QzNmzPDJtoqLi9WkSRNt2rTJJ9sD4Fu8kzEAn7BYLOU+/9hjj2n8+PEyDENxcXE109T/+Ne//qVevXrpwIEDio6O9sk2X375ZS1evNjtQwEB1A4EHAA+cfToUdfX8+fP17Rp07Rr1y7XsujoaJ8Fi8q4++67FRISorlz5/psm6dPn1ZSUpK2bNmi1q1b+2y7AKqOS1QAfCIpKcn1iI2NlcVicVsWHR193iWqq666Svfdd5/GjRununXrKjExUX/+85+Vl5enO++8UzExMUpLS9MXX3zhtq/vvvtO/fr1U3R0tBITE3XHHXfo5MmTZfbmcDj04Ycf6rrrrnNb3qRJE82YMUN33XWXYmJi1LhxY73xxhuu54uLizV27FglJycrPDxcKSkpmjlzpuv5unXr6vLLL9cHH3xQxZ8eAF8j4ADwq7ffflvx8fHasGGD7rvvPo0ZM0aDBw9Wjx49tGXLFl177bW64447lJ+fL0nKyspSr169lJ6erk2bNunLL7/UsWPHdMstt5S5j+3btys7O1udO3c+77lnn31WnTt31tatW3XPPfdozJgxrjNPL774oj799FMtWLBAu3bt0nvvvacmTZq4rd+lSxd9++23vvuBAPAJAg4Av2rfvr2mTp2q5s2ba8qUKQoPD1d8fLxGjhyp5s2ba9q0aTp16pS2b98u6ex9L+np6ZoxY4Zatmyp9PR0vfnmm1q+fLl27959wX0cOHBANptN9evXP++5/v3765577lFaWpomTZqk+Ph4LV++XJKUkZGh5s2b64orrlBKSoquuOIK/fa3v3Vbv0GDBjpw4ICPfyoAqoqAA8Cv2rVr5/raZrPpoosuUtu2bV3LEhMTJUnHjx+XdPZm4eXLl7vu6YmOjlbLli0lSXv37r3gPgoKCmS32y94I/T/7v/cZbVz+xo+fLi2bdumFi1a6P7779fXX3993voRERGus0sAao8QfzcAILiFhoa6fW+xWNyWnQslTqdTknTmzBldd911evrpp8/bVnJy8gX3ER8fr/z8fBUXFyssLKzC/Z/bV8eOHbVv3z598cUX+uabb3TLLbeod+/e+vDDD131mZmZSkhI8PRwAdQQAg6AgNKxY0ctWrRITZo0UUiIZ3+EdejQQZL0/fffu772VJ06dTRkyBANGTJEgwYNUt++fZWZmal69epJOnvDc3p6ulfbBFD9uEQFIKDce++9yszM1G9/+1tt3LhRe/fu1VdffaU777xTDofjguskJCSoY8eOWr16tVf7mjNnjt5//33t3LlTu3fv1sKFC5WUlOT2Pj7ffvutrr322qocEoBqQMABEFAaNGigNWvWyOFw6Nprr1Xbtm01btw4xcXFyWot+4+0u+++W++9955X+4qJidGsWbPUuXNnXXbZZdq/f78+//xz137Wrl2r7OxsDRo0qErHBMD3eKM/AEGhoKBALVq00Pz589W9e3efbHPIkCFq3769Hn74YZ9sD4DvcAYHQFCIiIjQO++8U+4bAnqjuLhYbdu21fjx432yPQC+xRkcAABgOpzBAQAApkPAAQAApkPAAQAApkPAAQAApkPAAQAApkPAAQAApkPAAQAApkPAAQAApkPAAQAApvP/c6W7u77/kIcAAAAASUVORK5CYII=",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "chrg_scan = ForLoopPT(ParallelChannelPT(linear_step, y_value), 'y_i', 'n_y')\n",
+ "\n",
+ "_ = plot(chrg_scan, parameters=sweep_params_2d, plot_measurements={'M'})\n",
+ "default_params = {\n",
+ " 'n_segments': 2,\n",
+ " 'x_start': 0,\n",
+ " 'x_stop': 3,\n",
+ " 'y_start': 0,\n",
+ " 'y_stop': 2,\n",
+ " 'N_x': 10,\n",
+ " 'N_y': 5,\n",
+ " 'sample_rate': 1,\n",
+ " 'cds_res': 5\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5aceae43",
+ "metadata": {},
+ "source": [
+ "Now, we use `plot` function to inspect the positive sweep of channel X which is highlighted in the following plot."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "71dd0045-292d-49f7-90ec-124b38d53566",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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RCSHFKSkSMrIyKBFixY888wz2dbHx8fj7e3Nv/71LwtVJiIFmeYO+V/6ZXMpso4fP07jxo2ZM2cOvXr1AqBPnz7s27ePHTt2YG9vb+EKRaQg0twhf6UgJUXajBkzePfddzl06BDbt2+nR48e7Nixg0aNGlm6NBEpwDR3yJ8UpKRIMwyDtm3bYmNjw4EDBxgxYgRvvfWWpcsSkQJOc4f8SUFKiryjR49Sp04dGjRowO7du7G1tbV0SSJiBTR3COhicxHmzZuHk5MTp0+fJjo62tLliIiV0NwhoDNSUsRt2bIFPz8/1qxZw4cffgjAunXrMJlMFq5MRAoyzR3yJ52RkiIrKSmJvn37MmTIEAICApg7dy7bt29n9uzZli5NRAowzR3yVzojJUXWa6+9xu+//86+fftwcnIC4Msvv2Ts2LEcOHCAypUrW7ZAESmQNHfIXylISZEUHh5Ou3btCAsLo1WrVtkeCwoKIj09XafpReQOmjvkfylIiYiIiOSQrpESERERySEFKREREZEcUpASERERySEFKREREZEcUpASERERySEFKREREZEcUpASERERySEFKREREZEcUpASERERySEFKREREZEcUpASERERySEFKREREZEc+n/x0cW/UPf5vgAAAABJRU5ErkJggg==",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from qupulse.plotting import plot_2d\n",
+ "\n",
+ "f, (ax1, ax2) = plt.subplots(1, 2, sharey=True)\n",
+ "ax1.set_title(\"Regular\")\n",
+ "ax2.set_title(\"Snake\")\n",
+ "_ = plot_2d(chrg_scan, ('X', 'Y'), parameters=sweep_params_2d, ax=ax1)\n",
+ "_ = plot_2d(snake_step, ('X', 'Y'), parameters=sweep_params_2d, ax=ax2)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "813d89e8",
+ "metadata": {},
+ "source": []
+ },
+ {
+ "cell_type": "markdown",
+ "id": "aa0df99d",
+ "metadata": {},
+ "source": [
+ "## Benchmark\n",
+ "\n",
+ "When we try to instantiate such a pulse it will take some time but not severe yet. However, a typical charge scan has a higher resolution. The qupulse parameter evaluation machinery can become a bottleneck here since it is run for every of the 100 points. There is work being done on optimizing it so see for yourself what the current status is."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "f7213346",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Elapsed time: 0.006888100004289299 seconds\n"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Elapsed time: 1.4369120999763254 seconds\n"
+ ]
+ }
+ ],
+ "source": [
+ "import timeit\n",
+ "\n",
+ "# Try with different resolution by yourself!\n",
+ "exp_params = {\n",
+ " 'tx_sweep': 10240,\n",
+ " 'x_start': -50e-3,\n",
+ " 'x_stop': 50e-3,\n",
+ " 'y_start': 0,\n",
+ " 'y_stop': 0.1, \n",
+ " 'n_x': 100, # voltage resolution: 1 mV\n",
+ " 'n_y': 100, # voltage resolution: 1 mV\n",
+ "}\n",
+ "\n",
+ "\n",
+ "# using arbitary parameters for simplicity.\n",
+ "simple_inst = timeit.timeit(lambda: snake_step.create_program(parameters=sweep_params_2d), number=1)\n",
+ "print(f'Elapsed time: {simple_inst} seconds')\n",
+ "\n",
+ "# in a real experiment:\n",
+ "exp_inst = timeit.timeit(lambda: snake_step.create_program(parameters=exp_params), number=1)\n",
+ "print(f'Elapsed time: {exp_inst} seconds')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "69c86045",
+ "metadata": {},
+ "source": [
+ "## Hardware limitations and legacy pulses\n",
+ "\n",
+ "This section is under construction and will give an example and reasoning why old pulses often contain\n",
+ "\n",
+ "`RepetitionPT(ConstantPT(...))`\n",
+ "\n",
+ "and how this was superseeded by the `qupulse._program._loop.roll_constant_waveforms` function."
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/doc/source/examples/03xComposedPulses.ipynb b/doc/source/examples/03xComposedPulses.ipynb
deleted file mode 100644
index e7b026c1a..000000000
--- a/doc/source/examples/03xComposedPulses.ipynb
+++ /dev/null
@@ -1,4165 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Combining Pulse Templates\n",
- "\n",
- "So far we have seen how to define simple pulses using the `TablePulseTemplate` ([Modelling a Simple TablePulseTemplate](00SimpleTablePulse.ipynb)), `FunctionPulseTemplate` ([Modelling Pulses Using Functions And Expressions](02FunctionPulse.ipynb)) and `PointPulseTemplate` ([The PointPulseTemplate](03PointPulse.ipynb)) classes. These are the elementary building blocks to create pulses and we call them *atomic* pulse templates.\n",
- "\n",
- "We will now have a look at how to compose more complex pulse structures.\n",
- "\n",
- "## SequencePulseTemplate: Putting Pulses in a Sequence\n",
- "\n",
- "As the name suggests `SequencePulseTemplate` allows us to define a pulse as a sequence of already existing pulse templates which are run one after another. In the following example we have two templates created using `PointPulseTemplate` and want to define a higher-level pulse template that puts them in sequence."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "sequence parameters: {'t_2', 'v_1', 'v_0', 't'}\n",
- "sequence measurements: {'M'}\n"
- ]
- }
- ],
- "source": [
- "from qupulse.pulses import PointPT, SequencePT\n",
- "# create our atomic \"low-level\" PointPTs\n",
- "first_point_pt = PointPT([(0, 'v_0'),\n",
- " (1, 'v_1', 'linear'),\n",
- " ('t', 'v_0+v_1', 'jump')],\n",
- " channel_names={'A'},\n",
- " measurements={('M', 1, 't-1')})\n",
- "second_point_pt = PointPT([(0, 'v_0+v_1'),\n",
- " ('t_2', 'v_0', 'linear')],\n",
- " channel_names={'A'},\n",
- " measurements={('M', 0, 1)})\n",
- "\n",
- "# define the SequencePT\n",
- "sequence_pt = SequencePT(first_point_pt, second_point_pt)\n",
- "\n",
- "print(\"sequence parameters: {}\".format(sequence_pt.parameter_names))\n",
- "print(\"sequence measurements: {}\".format(sequence_pt.measurement_names))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "It is important to note that all of the pulse templates used to create a `SequencePT` (we call those *subtemplates*) are defined on the same channels, in this case the channel `A` (otherwise we would encounter an exception). The `SequencePT` will also be defined on the same channel.\n",
- "\n",
- "The `SequencePT` will further have the union of all parameters defined in its subtemplates as its own parameter set. If two subtemplates defined parameters with the same name, they will be treated as the same parameters in the `SequencePT`.\n",
- "\n",
- "Finally, `SequencePT` will also expose all measurements defined in subtemplates. It is also possible to define additional measurements in the constructor of `SequencePT`. See [Definition of Measurements](08Measurements.ipynb) for me info about measurements.\n",
- "\n",
- "There are several cases where the above constraints represent a problem: Subtemplates might not all be defined on the same channel, subtemplates might define parameters with the same name which should still be treated as different parameters in the sequence or names of measurements defined by different subtemplates might collide. To deal with these, we can wrap a subtemplate with the `MappingPulseTemplate` class which allows us to rename parameters, channels and measurements or even derive parameter values from other parameters using mathematical expressions. You can learn how to do all this in [Mapping with the MappingPulseTemplate](05MappingTemplate.ipynb).\n",
- "\n",
- "In our example above, however, we were taking care not to encounter these problems yet. Let's plot all of them with some parameters to see the results."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "%matplotlib notebook\n",
- "from qupulse.pulses.plotting import plot\n",
- "\n",
- "parameters = dict(t=3,\n",
- " t_2=2,\n",
- " v_0=1,\n",
- " v_1=1.4)\n",
- "\n",
- "_ = plot(first_point_pt, parameters, sample_rate=100)\n",
- "_ = plot(second_point_pt, parameters, sample_rate=100)\n",
- "_ = plot(sequence_pt, parameters, sample_rate=100)\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## RepetitionPulseTemplate: Repeating a Pulse\n",
- "\n",
- "If we simply want to repeat some pulse template a fixed number of times, we can make use of the `RepetitionPulseTemplate`. In the following, we will reuse one of our `PointPT`s, `first_point_pt` and use it to create a new pulse template that repeats it `n_rep` times, where `n_rep` will be a parameter."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "repetition parameters: {'v_1', 'n_rep', 'v_0', 't'}\n",
- "repetition measurements: {'M'}\n"
- ]
- },
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "from qupulse.pulses import RepetitionPT\n",
- "\n",
- "repetition_pt = RepetitionPT(first_point_pt, 'n_rep')\n",
- "\n",
- "print(\"repetition parameters: {}\".format(repetition_pt.parameter_names))\n",
- "print(\"repetition measurements: {}\".format(repetition_pt.measurement_names))\n",
- "\n",
- "# let's plot to see the results\n",
- "parameters['n_rep'] = 5 # add a value for our n_rep parameter\n",
- "_ = plot(repetition_pt, parameters, sample_rate=100)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The same remarks that were made about `SequencePT` also hold for `RepetitionPT`: it will expose all parameters and measurements defined by its subtemplate and will be defined on the same channels.\n",
- "\n",
- "## ForLoopPulseTemplate: Repeat a Pulse with a Varying Loop Parameter\n",
- "\n",
- "The `RepetitionPT` simple repeats the exact same subtemplate a given number of times. Sometimes, however, it is rather required to vary the parameters of a subtemplate in a loop, for example when trying to determine the best value for a parameter of a given pulse. This is what the `ForLoopPulseTemplate` is intended for. As the name suggests, its behavior mimics that for `for-loop` constructs in programming languages by repeating its content - the subtemplate - for a number of times while at the same time supplying a loop parameter that iterates over a range of values.\n",
- "\n",
- "In the following we make use of this to vary the value of parameter `t` in `first_point_pt` over several iterations. More specifically, we will have all a `first_point_pt` pulse for all even values of `t` between `t_start` and `t_end` which are new parameters. For the plot we will set them to `t_start = 4` and `t_end = 13`, i.e., `t = 4, 6, 8, 10, 12`."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "for loop parameters: {'t_start', 'v_1', 'v_0', 't_end'}\n",
- "for loop measurements: {'M'}\n"
- ]
- },
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "from qupulse.pulses import ForLoopPT\n",
- "\n",
- "for_loop_pt = ForLoopPT(first_point_pt, 't', ('t_start', 't_end', 2))\n",
- "\n",
- "print(\"for loop parameters: {}\".format(for_loop_pt.parameter_names))\n",
- "print(\"for loop measurements: {}\".format(for_loop_pt.measurement_names))\n",
- "\n",
- "# plot it\n",
- "parameters['t_start'] = 4\n",
- "parameters['t_end'] = 13\n",
- "_ = plot(for_loop_pt, parameters, sample_rate=100)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The second argument to `ForLoopPT`'s constructor is the name of the loop parameter. This has to be a parameter that is defined by the subtemplate. The third argument defined the range of the loop. The syntax of the range is similar to that of the `range()` command in Python, i.e., a tuple `(start_value, end_value, step)`. As seen above, inserting parameter values or even expressions is okay. As in `range()`, the `end_value` is exclusive.\n",
- "\n",
- "As for `SequencePT` and `RepetitionPT`, `ForLoopPT` exposes all parameters defined by the subtemplate except for the loop parameter, `t` in the above example. If expressions are used in the range definition and they make use of additional parameters, these are also exposed by `ForLoopPT`. \n",
- "\n",
- "`ForLoopPT` also exposes measurements defined by subtemplates.\n",
- "\n",
- "## AtomicMultiChannelPulseTemplate: Run Pulses in Parallel on Different Channels\n",
- "\n",
- "So far we have only looked at pulses that affect the time-domain aspect of combining pulses. Another way to combine pulses is to parallelise them by executing them on different channels at the same time. This is of course already supported by simply creating atomic pulse templates (`TablePT`, `PointPT`, `FunctionPT`) on multiple channels. However, sometimes it is necessary to put already existing pulses in parallel. Instead of having to define a new atomic pulse template for this, we can make use of the `AtomicMuliChannelPulseTemplate` class. To learn more about how this works, see [Multi-Channel Pulses](07MultiChannelTemplates.ipynb).\n",
- "\n",
- "## Combining Combined Pulses\n",
- "\n",
- "Our examples above have build combined higher-level pulses (`SequencePT`, `RepetitionPT`, `ForLoopPT`) on atomic subtemplates only. However, this is not a requirement. We can use `SequencePT`, `RepetitionPT` and `ForLoopPT` using any `PulseTemplate` objects as subtemplates allowing us to build arbitrarily complex pulses out of only a handful of primitives."
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python [default]",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.7.0"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/doc/source/examples/15DynamicNuclearPolarisation.ipynb b/doc/source/examples/04DynamicNuclearPolarisation.ipynb
similarity index 78%
rename from doc/source/examples/15DynamicNuclearPolarisation.ipynb
rename to doc/source/examples/04DynamicNuclearPolarisation.ipynb
index 5f02c1219..db4b03ab8 100644
--- a/doc/source/examples/15DynamicNuclearPolarisation.ipynb
+++ b/doc/source/examples/04DynamicNuclearPolarisation.ipynb
@@ -44,7 +44,7 @@
},
{
"cell_type": "code",
- "execution_count": 3,
+ "execution_count": null,
"metadata": {},
"outputs": [],
"source": [
@@ -66,7 +66,7 @@
},
{
"cell_type": "code",
- "execution_count": 4,
+ "execution_count": null,
"metadata": {},
"outputs": [],
"source": [
@@ -89,20 +89,9 @@
},
{
"cell_type": "code",
- "execution_count": 5,
+ "execution_count": null,
"metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "LOOP 1 times:\n",
- " ->EXEC 3 times\n",
- " ->EXEC 3 times\n",
- " ->EXEC 3 times\n"
- ]
- }
- ],
+ "outputs": [],
"source": [
"hardware_setup.register_program('dnp', dnp_prog)\n",
"hardware_setup.arm_program('dnp')\n",
@@ -123,7 +112,7 @@
},
{
"cell_type": "code",
- "execution_count": 6,
+ "execution_count": null,
"metadata": {},
"outputs": [],
"source": [
@@ -139,20 +128,9 @@
},
{
"cell_type": "code",
- "execution_count": 7,
+ "execution_count": null,
"metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "LOOP 1 times:\n",
- " ->EXEC 3 times\n",
- " ->EXEC 1 times\n",
- " ->EXEC 5 times\n"
- ]
- }
- ],
+ "outputs": [],
"source": [
"used_awg.run_current_program()\n",
"\n",
@@ -168,22 +146,11 @@
}
],
"metadata": {
- "kernelspec": {
- "display_name": "Python 3",
- "language": "python",
- "name": "python3"
- },
"language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.7.4"
+ "name": "python"
+ },
+ "nbsphinx": {
+ "execute": "never"
}
},
"nbformat": 4,
diff --git a/doc/source/examples/04PulseStorage.ipynb b/doc/source/examples/04PulseStorage.ipynb
deleted file mode 100644
index 437778796..000000000
--- a/doc/source/examples/04PulseStorage.ipynb
+++ /dev/null
@@ -1,412 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Storing Pulse Templates: `PulseStorage` and Serialization\n",
- "\n",
- "So far, we have constructed new pulse templates in code for each session (which were discarded afterwards). We now want to store them persistently in the file system to be able to reuse them in later sessions. For this, qupulse offers us serialization and deserialization using the `PulseStorage` and `StorageBackend` classes.\n",
- "\n",
- "The pulse storage manages the (de-)serialization to JSON and requires a storage backend to persistently store the serialized data. This can for example be a `FilesystemBackend` or a `DictBackend`. Let us first use a `DictBackend` to inspect the serialized pulse.\n",
- "\n",
- "__Attention:__ Due to the fact that PulseStorage enforces unique identifiers, executing the cells in this notebook out of order or rerunning them will likely result in errors. You will have to restart the Kernel in that case.\n",
- "\n",
- "## Single Pulses\n",
- "First we will have a look at how to store pulses that do not contain other pulse templates. To store a pulse, __the pulse needs to have an identifier__. If you forgot to give the pulse an identifier one can use the `rename` method which returns a new pulse with the requested identifier.\n",
- "\n",
- "### Storing"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{'my_pulse': '{\\n'\n",
- " ' \"#identifier\": \"my_pulse\",\\n'\n",
- " ' \"#type\": '\n",
- " '\"qupulse.pulses.table_pulse_template.TablePulseTemplate\",\\n'\n",
- " ' \"entries\": {\\n'\n",
- " ' \"default\": [\\n'\n",
- " ' [\\n'\n",
- " ' \"t_begin\",\\n'\n",
- " ' \"v_begin\",\\n'\n",
- " ' \"hold\"\\n'\n",
- " ' ],\\n'\n",
- " ' [\\n'\n",
- " ' \"t_end\",\\n'\n",
- " ' \"v_end\",\\n'\n",
- " ' \"linear\"\\n'\n",
- " ' ]\\n'\n",
- " ' ]\\n'\n",
- " ' },\\n'\n",
- " ' \"measurements\": [],\\n'\n",
- " ' \"parameter_constraints\": []\\n'\n",
- " '}'}\n"
- ]
- }
- ],
- "source": [
- "import pprint\n",
- "from qupulse.pulses import TablePT\n",
- "from qupulse.serialization import PulseStorage, DictBackend\n",
- "\n",
- "dict_backend = DictBackend()\n",
- "dict_pulse_storage = PulseStorage(dict_backend)\n",
- "\n",
- "table_pulse = TablePT({'default': [('t_begin', 'v_begin', 'hold'),\n",
- " ('t_end', 'v_end', 'linear')]}, identifier='my_pulse')\n",
- "\n",
- "dict_pulse_storage['my_pulse'] = table_pulse\n",
- "\n",
- "pprint.pprint(dict_backend.storage)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Now to store this in a file system we need to replace the `DictBackend` with a `FilesystemBackend`. The following code will create the file `'./serialized_pulses/my_pulse.json'`."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {},
- "outputs": [],
- "source": [
- "from qupulse.serialization import FilesystemBackend\n",
- "\n",
- "filesystem_backend = FilesystemBackend('./serialized_pulses')\n",
- "file_pulse_storage = PulseStorage(filesystem_backend)\n",
- "\n",
- "if 'my_pulse' in file_pulse_storage:\n",
- " del file_pulse_storage['my_pulse']\n",
- "\n",
- "file_pulse_storage['my_pulse'] = table_pulse"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Loading\n",
- "Now we will load a pulse that is shipped only as a JSON file. It is a single sine with frequency `omega`. Note that loading the same pulse multiple times will give you the same object."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Loading the same pulse multiple times gives you the same object\n"
- ]
- },
- {
- "data": {
- "image/png": "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VYXPLVmyhPfff5/Zs2dz/PHHM3LkSOrUqcOLL75Io0aNWLFiBfvuuy/HH388APPmzeOZZ57hoYce4ic/+QnPP/88Z5xxBgDbtm1jypQpjB8/nuuvv56JEyfyyCOP0LhxYz7++GM2b97MAQccwJFHHlliw/eLL77InDlzmDFjBkuXLqVnz56cc845bN26lUsuuYRx48bRokULnn32Wa699loeffRRLrzwQho0aMBvf/tbAB599FGaNm3Kxo0b2XvvvTn55JNp1qxZmftx5MiR3HPPPdx+++3k5eUlvP+TQUVFRLLG22+/zciRI2nevDkATZs2LXzuhBNOoFq1avTs2ZOlS5cCwdHCNddcw6RJk6hWrRrfffdd4XMdO3akf//+AAwaNIiFCxcWbuukk07a5fEJEybw+eefFx4FrVmzhnnz5tG1a9dis06aNInTTjuN6tWr06ZNGw499FAA5syZwxdffMERRxwBBEdArVu3LnYbd911Fy+++CIA3377LfPmzUuoqEQp0qJiZo8CxwLL3H2Xli4L/gS4Ezga2ACMcvdP0ptSRIpT2hFFqrh7iUcGtWvX3mk9gKeffprly5czbdo0atasSYcOHQr7aMSvX7169Z1OEcWeq169Otu2bSvc5t13382wYcN2et/4YlRUcVndnV69ejF58uTSPirvvvsuEydOZPLkydSrV4+hQ4cWZo/fbqaNbhB1m8pjwFGlPD8c6BLeLgDuT0MmEclQhx12GM899xwrV64E2On0V3HWrFlDy5YtqVmzJu+88w6LFlV8dPdhw4Zx//33s3XrVgDmzp3L+vXrS1z/4IMPZsyYMWzfvp0lS5bwzjvvANCtWzeWL19eWFS2bt3KzJkzi83epEkT6tWrx+zZs/nwww8Ln2vVqhWzZs2ioKCg8EimqIYNG7J27doKf96KivRIxd0nmVmHUlYZATzhwZ8dH5rZbmbW2t2XpCWgZDR3Z9HKDSz9cRP1atWgfu3q7NmsPtWrqXNeVdWrVy+uvfZahgwZQvXq1RkwYACPPfZYieuffvrpHHfcceTl5dG/f3+6d+9e4fc+77zzWLhwIQMHDsTdadGiBS+99FKJ65944om8/fbb9OnTh65duzJkyBAAatWqxdixY7n00ktZs2YN27Zt4/LLL6dXr52P/I466igeeOAB+vbtS7du3dh3330Ln7vllls4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\n",
- "text/plain": [
- ""
- ]
- },
- "metadata": {
- "needs_background": "light"
- },
- "output_type": "display_data"
- }
- ],
- "source": [
- "%matplotlib inline\n",
- "import math\n",
- "from qupulse.pulses.plotting import plot as plot\n",
- "\n",
- "sine = file_pulse_storage['my_other_pulse']\n",
- "\n",
- "_ = plot(sine, {'omega': 2*math.pi}, sample_rate=1000, show=False)\n",
- "\n",
- "if sine is file_pulse_storage['my_other_pulse']:\n",
- " print('Loading the same pulse multiple times gives you the same object')"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Composed pulses and the role of identifiers\n",
- "If we have a pulse that contains other pulses all pulses that have an identifier are stored seperatly. Each `PulseStorage` instance expects that identifiers are unique (see below). Anonymous subpulses are stored together with their parent.\n",
- "\n",
- "We will now only use a dictionary as a backend it is easier to see what happens.\n",
- "\n",
- "### Storing"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{'combined': '{\\n'\n",
- " ' \"#identifier\": \"combined\",\\n'\n",
- " ' \"#type\": '\n",
- " '\"qupulse.pulses.sequence_pulse_template.SequencePulseTemplate\",\\n'\n",
- " ' \"subtemplates\": [\\n'\n",
- " ' {\\n'\n",
- " ' \"#type\": '\n",
- " '\"qupulse.pulses.repetition_pulse_template.RepetitionPulseTemplate\",\\n'\n",
- " ' \"body\": {\\n'\n",
- " ' \"#identifier\": \"my_other_pulse\",\\n'\n",
- " ' \"#type\": \"reference\"\\n'\n",
- " ' },\\n'\n",
- " ' \"repetition_count\": \"N_sine\"\\n'\n",
- " ' },\\n'\n",
- " ' {\\n'\n",
- " ' \"#identifier\": \"my_pulse\",\\n'\n",
- " ' \"#type\": \"reference\"\\n'\n",
- " ' }\\n'\n",
- " ' ]\\n'\n",
- " '}',\n",
- " 'my_other_pulse': '{\\n'\n",
- " ' \"#identifier\": \"my_other_pulse\",\\n'\n",
- " ' \"#type\": '\n",
- " '\"qupulse.pulses.function_pulse_template.FunctionPulseTemplate\",\\n'\n",
- " ' \"channel\": \"default\",\\n'\n",
- " ' \"duration_expression\": \"2*pi/omega\",\\n'\n",
- " ' \"expression\": \"sin(omega*t)\",\\n'\n",
- " ' \"measurements\": [],\\n'\n",
- " ' \"parameter_constraints\": []\\n'\n",
- " '}',\n",
- " 'my_pulse': '{\\n'\n",
- " ' \"#identifier\": \"my_pulse\",\\n'\n",
- " ' \"#type\": '\n",
- " '\"qupulse.pulses.table_pulse_template.TablePulseTemplate\",\\n'\n",
- " ' \"entries\": {\\n'\n",
- " ' \"default\": [\\n'\n",
- " ' [\\n'\n",
- " ' \"t_begin\",\\n'\n",
- " ' \"v_begin\",\\n'\n",
- " ' \"hold\"\\n'\n",
- " ' ],\\n'\n",
- " ' [\\n'\n",
- " ' \"t_end\",\\n'\n",
- " ' \"v_end\",\\n'\n",
- " ' \"linear\"\\n'\n",
- " ' ]\\n'\n",
- " ' ]\\n'\n",
- " ' },\\n'\n",
- " ' \"measurements\": [],\\n'\n",
- " ' \"parameter_constraints\": []\\n'\n",
- " '}'}\n"
- ]
- }
- ],
- "source": [
- "from qupulse.pulses import RepetitionPT, SequencePT\n",
- "\n",
- "# anonymous pulse template\n",
- "repeated_sine = RepetitionPT(sine, 'N_sine')\n",
- "\n",
- "my_sequence = SequencePT(repeated_sine, table_pulse, identifier='combined')\n",
- "\n",
- "dict_pulse_storage['combined'] = my_sequence\n",
- "\n",
- "pprint.pprint(dict_backend.storage)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "As you see, the serialization of 'combined' explicitly contains the anonymous `RepetitionPulseTemplate` but references to 'my_pulse' and 'my_other_pulse' which are stored as separate entries.\n",
- "\n",
- "## Pulse registry and unique identifiers\n",
- "There is the possibility to store pulse templates on construction into a pulse registry. This can be a `PulseStorage`. To set a pulse storage as the default pulse registry call `set_to_default_registry`"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{'combined': '{\\n'\n",
- " ' \"#identifier\": \"combined\",\\n'\n",
- " ' \"#type\": '\n",
- " '\"qupulse.pulses.sequence_pulse_template.SequencePulseTemplate\",\\n'\n",
- " ' \"subtemplates\": [\\n'\n",
- " ' {\\n'\n",
- " ' \"#type\": '\n",
- " '\"qupulse.pulses.repetition_pulse_template.RepetitionPulseTemplate\",\\n'\n",
- " ' \"body\": {\\n'\n",
- " ' \"#identifier\": \"my_other_pulse\",\\n'\n",
- " ' \"#type\": \"reference\"\\n'\n",
- " ' },\\n'\n",
- " ' \"repetition_count\": \"N_sine\"\\n'\n",
- " ' },\\n'\n",
- " ' {\\n'\n",
- " ' \"#identifier\": \"my_pulse\",\\n'\n",
- " ' \"#type\": \"reference\"\\n'\n",
- " ' }\\n'\n",
- " ' ]\\n'\n",
- " '}',\n",
- " 'my_other_pulse': '{\\n'\n",
- " ' \"#identifier\": \"my_other_pulse\",\\n'\n",
- " ' \"#type\": '\n",
- " '\"qupulse.pulses.function_pulse_template.FunctionPulseTemplate\",\\n'\n",
- " ' \"channel\": \"default\",\\n'\n",
- " ' \"duration_expression\": \"2*pi/omega\",\\n'\n",
- " ' \"expression\": \"sin(omega*t)\",\\n'\n",
- " ' \"measurements\": [],\\n'\n",
- " ' \"parameter_constraints\": []\\n'\n",
- " '}',\n",
- " 'my_pulse': '{\\n'\n",
- " ' \"#identifier\": \"my_pulse\",\\n'\n",
- " ' \"#type\": '\n",
- " '\"qupulse.pulses.table_pulse_template.TablePulseTemplate\",\\n'\n",
- " ' \"entries\": {\\n'\n",
- " ' \"default\": [\\n'\n",
- " ' [\\n'\n",
- " ' \"t_begin\",\\n'\n",
- " ' \"v_begin\",\\n'\n",
- " ' \"hold\"\\n'\n",
- " ' ],\\n'\n",
- " ' [\\n'\n",
- " ' \"t_end\",\\n'\n",
- " ' \"v_end\",\\n'\n",
- " ' \"linear\"\\n'\n",
- " ' ]\\n'\n",
- " ' ]\\n'\n",
- " ' },\\n'\n",
- " ' \"measurements\": [],\\n'\n",
- " ' \"parameter_constraints\": []\\n'\n",
- " '}',\n",
- " 'new_pulse': '{\\n'\n",
- " ' \"#identifier\": \"new_pulse\",\\n'\n",
- " ' \"#type\": '\n",
- " '\"qupulse.pulses.function_pulse_template.FunctionPulseTemplate\",\\n'\n",
- " ' \"channel\": \"default\",\\n'\n",
- " ' \"duration_expression\": 1,\\n'\n",
- " ' \"expression\": 0,\\n'\n",
- " ' \"measurements\": [],\\n'\n",
- " ' \"parameter_constraints\": []\\n'\n",
- " '}'}\n"
- ]
- }
- ],
- "source": [
- "from qupulse.pulses import FunctionPT\n",
- "\n",
- "dict_pulse_storage.set_to_default_registry()\n",
- "\n",
- "new_pulse = FunctionPT(0, 1, identifier='new_pulse')\n",
- "\n",
- "pprint.pprint(dict_backend.storage)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "As you see each newly created pulse is put into the pulse storage. Creating a new pulse with the same name will fail:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 6,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Oh No!!!\n",
- "('Pulse with name already exists', 'new_pulse')\n",
- "\n"
- ]
- }
- ],
- "source": [
- "try:\n",
- " new_pulse = FunctionPT(0, 1, identifier='new_pulse')\n",
- "except RuntimeError as err:\n",
- " print('Oh No!!!')\n",
- " print(err)\n",
- " print()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We have to either explicitly overwrite the registry or delete the old pulse from it:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 7,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Overwriting the registry works!\n",
- "Deleting the pulse works, too!\n"
- ]
- }
- ],
- "source": [
- "try:\n",
- " new_pulse = FunctionPT(0, 1, identifier='new_pulse', registry=dict())\n",
- "except:\n",
- " raise\n",
- "else:\n",
- " print('Overwriting the registry works!')\n",
- "\n",
- "del dict_pulse_storage['new_pulse']\n",
- "try:\n",
- " new_pulse = FunctionPT(0, 1, identifier='new_pulse')\n",
- "except:\n",
- " raise\n",
- "else:\n",
- " print('Deleting the pulse works, too!')"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python [default]",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.7.0"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/doc/source/examples/04ZurichInstrumentsSetup.ipynb b/doc/source/examples/04ZurichInstrumentsSetup.ipynb
new file mode 100644
index 000000000..ada1e39fe
--- /dev/null
+++ b/doc/source/examples/04ZurichInstrumentsSetup.ipynb
@@ -0,0 +1,458 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "7e31fbc9f47a77ce",
+ "metadata": {
+ "collapsed": false
+ },
+ "source": [
+ "# Zurich Instruments Hardware Setup\n",
+ "\n",
+ "This notebook shows an exemplary use of qupulse with a ZI HDAWG and MFLI. The drivers for these instruments are kept in external packages to facilitate easy driver customization. Depending on your python version and hardware version you either need `qupulse-hdawg-legacy` or `qupulse-hdawg` for the HDAWG and `qupulse-mfli` for the MFLI.\n",
+ "\n",
+ "## Connections and wiring\n",
+ "\n",
+ "The example here assumes a very nonsensical wiring that does not require anything else besides an HDAWG, and MFLI and three cables/adapters to connect SMB to BNC ports. We assume the following connections:\n",
+ "\n",
+ "```\n",
+ "HDAWG_1_WAVE -> MFLI_AUX_IN_1\n",
+ "HDAWG_2_WAVE -> MFLI_AUX_IN_2\n",
+ "HDAWG_1_MARK_FRONT -> MFLI_TRIG_IN_1\n",
+ "```\n",
+ "`MFLI_TRIG_IN_1` is located on the back of the device.\n",
+ "\n",
+ "## Hardware Setup\n",
+ "\n",
+ "The hardware setup class provides a layer to map output channels to an arbitrary number of physical channels.\n",
+ "It also provides a mapping of measurement windows to specific dac instruments."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6432f1ccf75c7d58",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from qupulse.hardware.setup import HardwareSetup\n",
+ "\n",
+ "hw_setup = HardwareSetup()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "initial_id",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# This abstracts over possibly installed hdawg drivers\n",
+ "from qupulse.hardware.awgs.zihdawg import HDAWGRepresentation\n",
+ "\n",
+ "awg_serial = 'DEVXXXX'\n",
+ "assert awg_serial != 'DEVXXXX', \"Please enter the serial of a connected HDAWG\"\n",
+ "\n",
+ "hdawg = HDAWGRepresentation(awg_serial, 'USB' )"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4f15ba19d0961dbb",
+ "metadata": {
+ "collapsed": false
+ },
+ "source": [
+ "### Channel groupings\n",
+ "\n",
+ "The `AWG` class abstracts over a set of dependently programmable channels. The HDAWG supports multiple channel groupings which decouples individual channel groups. The most robust setting for qupulse is to use the `1x8` channel grouping which executes the same sequencing program on all channels and only differs in the waveform data that is sequenced. This results in a single channel tuple/`AWG` object which represents all eight channels.\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "eb9a838c161c244d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from qupulse.hardware.awgs.zihdawg import HDAWGChannelGrouping\n",
+ "from qupulse.hardware.setup import PlaybackChannel, MarkerChannel\n",
+ "\n",
+ "hdawg.channel_grouping = HDAWGChannelGrouping.CHAN_GROUP_1x8\n",
+ "awg, = hdawg.channel_tuples\n",
+ "\n",
+ "# here we assume plunger one and two are connected to the two first channels of the AWG\n",
+ "# It is considered best practice to use such names that relate to the connected sample gates\n",
+ "hw_setup.set_channel('P1', PlaybackChannel(awg, 0))\n",
+ "hw_setup.set_channel('P2', PlaybackChannel(awg, 1))\n",
+ "\n",
+ "# We connect the trigger to the marker output of the first channel\n",
+ "hw_setup.set_channel('Trig', MarkerChannel(awg, 0))\n",
+ "\n",
+ "# We can assign the same channel to multiple identifiers. Here we just assign all channels to a hardware name\n",
+ "for channel_idx, channel_letter in enumerate('ABCDEFGH'):\n",
+ " channel_name = f\"{hdawg.serial}_{channel_letter}\"\n",
+ " hw_setup.set_channel(channel_name, PlaybackChannel(awg, channel_idx), allow_multiple_registration=True)\n",
+ "\n",
+ "# We can also assign multiple channels to the same identifier\n",
+ "hw_setup.set_channel(f\"{hdawg.serial}_ALL\", [PlaybackChannel(awg, idx) for idx in range(8)], allow_multiple_registration=True)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "10ada657dd098fc7",
+ "metadata": {
+ "collapsed": false
+ },
+ "source": [
+ "### MFLI\n",
+ "\n",
+ "Next we will connect the MFLI."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "99e3edfcdf4ff697",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from qupulse_mfli.mfli import MFLIDAQ, postprocessing_average_within_windows\n",
+ "\n",
+ "mfli_serial = 'DEVXXXX'\n",
+ "assert mfli_serial != 'DEVXXXX', \"Please enter the serial of a connected MFLI\"\n",
+ "\n",
+ "mfli = MFLIDAQ.connect_to(mfli_serial)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "bf2a9ed85290c479",
+ "metadata": {
+ "collapsed": false
+ },
+ "source": [
+ "### Measurement masks\n",
+ "\n",
+ "qupulse has multiple layers where measurements are mapped. The hardware setup can map measurement windows to potentially multiple measurement masks, which are a combination of an instrument and an instrument specific identifier."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6afb0d40c704a02",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from qupulse.hardware.setup import MeasurementMask\n",
+ "\n",
+ "hw_setup.set_measurement('SET1', MeasurementMask(mfli, 'AverageAux1'))\n",
+ "hw_setup.set_measurement('SET2', MeasurementMask(mfli, 'AverageAux2'))\n",
+ "hw_setup.set_measurement('SET_ALL', [MeasurementMask(mfli, 'AverageAux1'), MeasurementMask(mfli, 'AverageAux2')], allow_multiple_registration=True)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e7353faf52ebd31d",
+ "metadata": {},
+ "source": [
+ "Each instrument can do arbitrary things with the identifier from the mask which heavily depends on what the instrument can do and what you use it for.\n",
+ "\n",
+ "The MLFI maps the names to internal paths following your configuration. You can make the configuration global or program specific."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c943fb4d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# linking the measurement mask names to physical input channels\n",
+ "mfli.register_measurement_channel(program_name=None, channel_path=\"demods/0/sample.AuxIn0\", window_name=\"AverageAux2\")\n",
+ "mfli.register_measurement_channel(program_name=None, channel_path=\"auxins/0/sample.AuxIn1\", window_name=\"AverageAux1\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9bb6bf76c80276e9",
+ "metadata": {},
+ "source": [
+ "The other inputs can be addressed via strings as the following:\n",
+ "```\n",
+ "{\n",
+ " \"R\": [\"demods/0/sample.R\"],\n",
+ " \"X\": [\"demods/0/sample.X\"],\n",
+ " \"Y\": [\"demods/0/sample.Y\"],\n",
+ " \"A\": [\"auxins/0/sample.AuxIn0.avg\"],\n",
+ " \"many\": [\"demods/0/sample.R\", \"auxins/0/sample.AuxIn0.avg\", \"demods/0/sample.X\", \"demods/0/sample.Y\"]\n",
+ "}\n",
+ "```\n",
+ "where the keys of the dict are the values for the window_name, and the values of the dict are the channel_path inputs. Note that these can also be lists to record multiple channels under one name. I.e. for IQ demodulation.\n",
+ "\n",
+ "### Operations\n",
+ "\n",
+ "Each driver can automatically perform certain operations on the recorded data. The MFLI expects a callable that processes the raw data returned by the instrument. This is suboptimal but the current solution. If you want to implement your own operation look at the shipped postprocessing functions for the signature.\n",
+ "\n",
+ "There are other functions you can use defined in the mfli package like `postprocessing_crop_windows`. Please file an issue if this documentation here is out of date."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "51401e51",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# configuring the driver to average all datapoint for each window.\n",
+ "mfli.register_operations(\n",
+ " program_name=None,\n",
+ " operations=postprocessing_average_within_windows\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ee6314ed25574b8f",
+ "metadata": {},
+ "source": []
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "417c4976",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# registering trigger settings for a standard configuration\n",
+ "# The measurement is perfomed once after one trigger on TrigIn1 is observed.\n",
+ "mfli.register_trigger_settings(program_name=None,\n",
+ " trigger_input=f\"demods/0/sample.TrigIn1\", # here TrigInN referrers to the printer label N\n",
+ " edge=\"rising\",\n",
+ " trigger_count=1,\n",
+ " level=.5,\n",
+ " measurement_count=1,\n",
+ " other_settings={\"holdoff/time\": 1e-3}\n",
+ " ) "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a7c5b77d781b5ab2",
+ "metadata": {
+ "collapsed": false
+ },
+ "source": [
+ "## Pulse definition\n",
+ "\n",
+ "Next we define a pulse that we want to use. We settle for a two-dimensional scan of a voltage space but we define the scan in terms of virtual gates, i.e. the potentials that the quantum dots `Q1` and `Q2` see.\n",
+ "Then we provide a linear transformation that maps them to the output voltages `P1` and `P2`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "27610ca4eb6cda25",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from qupulse.pulses import *\n",
+ "import numpy as np\n",
+ "from qupulse.program.transformation import LinearTransformation\n",
+ "from qupulse.program.loop import Loop, LoopBuilder, roll_constant_waveforms\n",
+ "\n",
+ "awg_sample_rate = 10**9\n",
+ "hdawg.set_sample_clock(awg_sample_rate)\n",
+ "\n",
+ "pt = (ConstantPT(2**20, {\n",
+ " 'Q1': '-0.1 + x_i * 0.02',\n",
+ " 'Q2': '-0.2 + y_i * 0.01'}, measurements=[('meas', 0, 2**20)])\n",
+ " .with_iteration('x_i', 'N_x')\n",
+ " .with_iteration('y_i', 'N_y')\n",
+ " .with_parallel_channels({'Marker': 1}))\n",
+ "\n",
+ "trafo = LinearTransformation(np.array([[1., -.1], [-.09, 1.]])*0.5,\n",
+ " ('Q1', 'Q2'),\n",
+ " ('P1', 'P2'))\n",
+ "\n",
+ "measurement_mapping = {'meas': 'SET_ALL'}\n",
+ "\n",
+ "# we chose the default LoopBuilder program builder here as it is the only supported as the time of writing this example \n",
+ "program: Loop = pt.create_program(parameters={'N_x': 20, 'N_y': 30},\n",
+ " global_transformation=trafo,\n",
+ " program_builder=LoopBuilder(),\n",
+ " measurement_mapping=measurement_mapping,\n",
+ " channel_mapping={'Marker': 'Trig'})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cb014734179113dd",
+ "metadata": {},
+ "source": [
+ "## HDAWG: Waveform compression and sample rate reduction\n",
+ "\n",
+ "The HDAWG has the capability to dynamically reduce the sample rate by a power of two during playback. The driver does this automatically if it detects a compatible waveform that is (piecewise) constant.\n",
+ "\n",
+ "However, the current implementation samples all waveforms in the computer memory. We have a lot (N_x * N_y) of very long waveforms which each take 4 MB in computer memory when sampled with 1GHz. For a sufficiently high resolution this will eat up our RAM with constant waveforms. qupulse provides `roll_constant_waveforms` to detect long constant waveforms and roll them into loops **inplace** if possible with the given parameters. This will remove the measurements from the `Loop` program because they cannot be preserved by the logic. Therefore, we extract them beforehand."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "435420307d15bfd2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# extract measurement positions\n",
+ "measurements = program.get_measurement_windows(drop=True)\n",
+ "\n",
+ "print(f'Single point before rolling: {program[0]!r}')\n",
+ "\n",
+ "# Compress program\n",
+ "roll_constant_waveforms(program, sample_rate=awg_sample_rate / 10**9, waveform_quantum=256, minimal_waveform_quanta=16)\n",
+ "\n",
+ "print(f'Single point after rolling: {program[0]!r}')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "346dabd84ea976fd",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "hw_setup.clear_programs()\n",
+ "hw_setup.register_program('csd', program, awg.run_current_program, measurements=measurements)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5d7d63d18bd6fc25",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "hdawg.output(1, True)\n",
+ "hdawg.output(2, True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "832c501c9082dc5d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "hw_setup.arm_program('csd')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "954f4e09664d073f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "hw_setup.run_program('csd')\n",
+ "import time; time.sleep(float(program.duration) / 1e9)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f4537c8c741597b0",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "hdawg.output(1, False)\n",
+ "hdawg.output(2, False)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e517992b6a35fc07",
+ "metadata": {
+ "collapsed": false
+ },
+ "source": [
+ "The data extration is not standardized at the time of writing this example because it heavily depends on your data processing pipeline how the data is handled and where it shall go. qupulse has no functionality to associate a measured value with the value of some parameter that might have been varied during the measurement."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "636ea90347e59fe5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# receaving the recorded data from the MFLI\n",
+ "\n",
+ "data = mfli.measure_program(wait=True) # wait=True would wait until the aquisition is finished.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "90137bb3a5dd2f22",
+ "metadata": {
+ "collapsed": false
+ },
+ "source": [
+ "The recorded data is sliced to the measurement windows in the default configuration. Thus ```my_lockin.measure_program``` returns a list (number of measurements) of dicts (the qupulse channels), of dicts (the lockin channels), of lists (the observed trigger), of lists of xarray DataArrays (each DataArray containing the data sliced for one window) or numpy arrays (containing the data resulting from averaging over the windows). I.e. ```returned_data[][][][]``` leads to ether the list of DataArrays or to a numpy array."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d3aa849c80214139",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "data_0 = data[0]\n",
+ "(average_1,), = data_0['AverageAux1'].values()\n",
+ "(average_2,), = data_0['AverageAux2'].values()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "fee1a294623c50e8",
+ "metadata": {},
+ "source": [
+ "Warning: As the time of writing this example there are problems when no demodulator is used at all. One channel looks like it has a sliding window average. Contribution in fixing that is highly appreciated."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "bc7de11e789884f2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "\n",
+ "plt.plot(average_1, '*')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "14c5cd608d2238a3",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "plt.plot(average_2)"
+ ]
+ }
+ ],
+ "metadata": {
+ "nbsphinx": {
+ "execute": "never"
+ },
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/doc/source/examples/05MappingTemplate.ipynb b/doc/source/examples/05MappingTemplate.ipynb
deleted file mode 100644
index f0de8ed4c..000000000
--- a/doc/source/examples/05MappingTemplate.ipynb
+++ /dev/null
@@ -1,1074 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Mapping with the MappingPulseTemplate\n",
- "\n",
- "We will now have a look on how to remap parameters, channel ids and measurements. The definition of measurements is illustrated in [Definition of Measurements](08Measurements.ipynb). The `MappingPulseTemplate` class allows us to take any already existing `PulseTemplate` and specify a mapping for its parameters, channel ids and measurements. \n",
- "\n",
- "This can be useful for simply renaming things, e.g., to avoid name collisions of parameters or change the name of a channel a pulse should be executed on, but can also be employed to derive the value of certain parameters from other parameters.\n",
- "\n",
- "## Mapping Parameters"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "2*pi/omega\n",
- "{'omega', 'a'}\n"
- ]
- }
- ],
- "source": [
- "from qupulse.pulses import MappingPT, FunctionPT, SequencePT, AtomicMultiChannelPT\n",
- "\n",
- "sine = FunctionPT('a*sin(omega*t)', 't_duration')\n",
- "\n",
- "my_parameter_mapping = dict(t_duration='2*pi/omega', omega='omega', a='a')\n",
- "\n",
- "single_period_sine = MappingPT(sine, parameter_mapping=my_parameter_mapping)\n",
- "\n",
- "print(single_period_sine.duration)\n",
- "print(single_period_sine.parameter_names)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Notice that we had to give mappings for all parameters, not only for the ones which changed. If we omit some of the encapsulated pulse tempaltes parameters an `MissingMappingException` is raised. This is done to enforce active thinking.\n",
- "\n",
- "You can, however, allow partial parameter mappings by passing `allow_partial_paramter_mappings=True` to the constructor."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "we expect an exception here:\n",
- "MissingMappingException : The template needs a mapping function for parameter(s) {'omega', 'a'}\n",
- "\n",
- "no exception with allow_partial_parameter_mapping=True\n",
- "2*pi/omega\n",
- "{'omega', 'a'}\n"
- ]
- }
- ],
- "source": [
- "partial_parameter_mapping = dict(t_duration='2*pi/omega')\n",
- "print('we expect an exception here:')\n",
- "try:\n",
- " single_period_sine = MappingPT(sine, parameter_mapping=partial_parameter_mapping)\n",
- "except Exception as exception:\n",
- " print(type(exception).__name__, ':', exception)\n",
- "print('')\n",
- "\n",
- "print('no exception with allow_partial_parameter_mapping=True')\n",
- "single_period_sine = MappingPT(sine, parameter_mapping=partial_parameter_mapping, allow_partial_parameter_mapping=True)\n",
- "print(single_period_sine.duration)\n",
- "print(single_period_sine.parameter_names)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Mapping of Channel Ids and Measurement Names\n",
- "\n",
- "Sometimes it is necessary to rename channels or measurements. Here we see a case where we want to play a sine and a cosine in parallel by using the `AtomicMultiChannelPulseTemplate` (for a more in depth explanation of multi-channel pulse template, see [Multi-Channel Pulses](07MultiChannelTemplates.ipynb)). Of course, this doesn't work as both pulses are by default defined on the 'default' channel."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "ChannelMappingException : Channel is defined in subtemplate 1 and subtemplate 2\n"
- ]
- }
- ],
- "source": [
- "sine_measurements = [('M', 't_duration/2', 't_duration')]\n",
- "sine = FunctionPT('a*sin(omega*t)', 't_duration', measurements=sine_measurements)\n",
- "\n",
- "cos_measurements = [('M', 0, 't_duration/2')]\n",
- "cos = FunctionPT('a*cos(omega*t)', 't_duration', measurements=cos_measurements)\n",
- "\n",
- "try:\n",
- " both = AtomicMultiChannelPT(sine, cos)\n",
- "except Exception as exception:\n",
- " print(type(exception).__name__, ':', exception)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The solution is to use the `MappingPT` and rename the channels as we see in the next cell. Additionally, we want to distinguish between the measurements, so we rename them, too. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "remapped_cos channels: {'cos_channel'}\n",
- "remapped_cos measurements: {'M_cos'}\n",
- "\n",
- "remapped_sine channels: {'sin_channel'}\n",
- "remapped_sine measurements: {'M_sin'}\n",
- "\n",
- "{'sin_channel', 'cos_channel'}\n",
- "{'M_cos', 'M_sin'}\n"
- ]
- }
- ],
- "source": [
- "cos_channel_mapping = dict(default='cos_channel')\n",
- "cos_measurement_mapping = dict(M='M_cos')\n",
- "remapped_cos = MappingPT(cos, channel_mapping=cos_channel_mapping, measurement_mapping=cos_measurement_mapping)\n",
- "print('remapped_cos channels:', remapped_cos.defined_channels)\n",
- "print('remapped_cos measurements:', remapped_cos.measurement_names)\n",
- "print()\n",
- "\n",
- "sine_channel_mapping = dict(default='sin_channel')\n",
- "sine_measurement_mapping = dict(M='M_sin')\n",
- "remapped_sine = MappingPT(sine, measurement_mapping=sine_measurement_mapping, channel_mapping=sine_channel_mapping)\n",
- "print('remapped_sine channels:', remapped_sine.defined_channels)\n",
- "print('remapped_sine measurements:', remapped_sine.measurement_names)\n",
- "print()\n",
- "\n",
- "both = AtomicMultiChannelPT(remapped_sine, remapped_cos)\n",
- "print(both.defined_channels)\n",
- "print(both.measurement_names)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let's also plot it to see if it looks like expected with some dummy values for our parameters:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "%matplotlib notebook\n",
- "from qupulse.pulses.plotting import plot\n",
- "\n",
- "parameter_values = dict(omega=1.0, a=1.0, t_duration=2*3.1415)\n",
- "\n",
- "_ = plot(both, parameters=parameter_values, sample_rate=100)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Automatically created mapping templates\n",
- "\n",
- "Besides the explicit usage of the template it is also used implicitly in some cases. All implicit uses make use of the static member function `MappingPulseTemplate.from_tuple`. This 'constructor' automatically decides which mapping belongs to which entity."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 6,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "channels: {'default'}\n",
- "measurements {'M_sin'}\n",
- "parameters {'t_duration', 'omega', 'a'}\n",
- "\n",
- "channels: {'default'}\n",
- "measurements {'M_sin'}\n",
- "parameters {'omega', 'a'}\n",
- "\n"
- ]
- }
- ],
- "source": [
- "auto_mapped = MappingPT.from_tuple((sine, sine_measurement_mapping))\n",
- "print('channels:', auto_mapped.defined_channels)\n",
- "print('measurements', auto_mapped.measurement_names)\n",
- "print('parameters', auto_mapped.parameter_names)\n",
- "print()\n",
- "\n",
- "auto_mapped = MappingPT.from_tuple((sine, sine_measurement_mapping, partial_parameter_mapping))\n",
- "print('channels:', auto_mapped.defined_channels)\n",
- "print('measurements', auto_mapped.measurement_names)\n",
- "print('parameters', auto_mapped.parameter_names)\n",
- "print()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "In many cases, you do not need to create the MappingPT yourself. Most PulseParameters accept a mapping tuple like the ones used in the last cell. We could create our combined pulse also by using this implicit conversion:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 7,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{'sin_channel', 'cos_channel'}\n",
- "{'M_cos', 'M_sin'}\n"
- ]
- }
- ],
- "source": [
- "both_implicit = AtomicMultiChannelPT((sine, sine_channel_mapping, sine_measurement_mapping), \n",
- " (cos, cos_measurement_mapping, cos_channel_mapping))\n",
- "print(both_implicit.defined_channels)\n",
- "print(both_implicit.measurement_names)"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python [default]",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.7.0"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/doc/source/examples/06CreatePrograms.ipynb b/doc/source/examples/06CreatePrograms.ipynb
deleted file mode 100644
index 251769ad5..000000000
--- a/doc/source/examples/06CreatePrograms.ipynb
+++ /dev/null
@@ -1,221 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Instantiating Pulses: Obtaining Pulse Instances From Pulse Templates\n",
- "\n",
- "In the previous examples, we have modelled pulses using the basic members of qupulse's `PulseTemplate` class hierarchy. However, these are only templates (or classes) of pulses and may contain parameters so that they cannot be run directly on hardware (this is also the reason why we always have to provide some parameters during plotting). First, we have to instantiate a concrete pulse in a process we call *instantiating*. We achieve this by making use of the `create_program()` method and will need to provide concrete parameter values.\n",
- "\n",
- "The example should be mostly self-contained and easy to follow, however, if you started here and don't know what pulse templates are and how to create them, maybe it's best to have a look at [Modelling a Simple TablePulseTemplate](00SimpleTablePulse.ipynb) first.\n",
- "\n",
- "To start, let us first create a pulse template with a few parameters and two channels.\n",
- "## Instantiating a TablePulse"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "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\n",
- "text/plain": [
- ""
- ]
- },
- "metadata": {
- "needs_background": "light"
- },
- "output_type": "display_data"
- }
- ],
- "source": [
- "%matplotlib inline\n",
- "from qupulse.pulses.plotting import plot\n",
- "from qupulse.pulses import TablePT\n",
- "template = TablePT(entries={'A': [(0, 0),\n",
- " ('ta', 'va', 'hold'),\n",
- " ('tb', 'vb', 'linear'),\n",
- " ('tend', 0, 'jump')],\n",
- " 'B': [(0, 0),\n",
- " ('ta', '-va', 'hold'),\n",
- " ('tb', '-vb', 'linear'),\n",
- " ('tend', 0, 'jump')]}, measurements=[('m', 0, 'ta'),\n",
- " ('n', 'tb', 'tend-tb')])\n",
- "\n",
- "parameters = {'ta': 2,\n",
- " 'va': 2,\n",
- " 'tb': 4,\n",
- " 'vb': 3,\n",
- " 'tc': 5,\n",
- " 'td': 11,\n",
- " 'tend': 6}\n",
- "_ = plot(template, parameters, sample_rate=100, show=False, plot_measurements={'m', 'n'})\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The `HardwareSetup` class represents the actual hardware and interfaces to the devices in qupulse. It is thus responsible for uploading to and executing pulses on the hardware. To do so it currently expects an instantiated pulse which is represented by `Loop` objects. These can be obtained by plugging the desired parameters into the `create_program` method of your `PulseTemplate` object."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "LOOP 1 times:\n",
- " ->EXEC 1 times\n",
- "Defined on {'B', 'A'}\n",
- "{'m': (array([0.]), array([2.])), 'n': (array([4.]), array([2.]))}\n"
- ]
- }
- ],
- "source": [
- "program = template.create_program(parameters=parameters,\n",
- " channel_mapping={'A': 'A', 'B': 'B'})\n",
- "\n",
- "print(program)\n",
- "print('Defined on', program[0].waveform.defined_channels)\n",
- "print(program.get_measurement_windows())"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The output shows us that a simple `Loop` object was created which just executes a single waveform without repetitions, just as our `PulseTemplate` specifies. In the `Loop` object all parameter references from the template have been resolved and replaced by the values provided in the `parameters` dictionary, so this is our pulse ready to be executed on the hardware.\n",
- "\n",
- "### Mapping Channels and Measurements During Instantiation\n",
- "\n",
- "The `channel_mapping` keyword argument allows us to rename channels or to drop them by mapping them to `None`. We can do the same to measurements using the `measurement_mapping` keyword argument."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "LOOP 1 times:\n",
- " ->EXEC 1 times\n",
- "Defined on {'Y'}\n",
- "{'foo': (array([0.]), array([2.]))}\n"
- ]
- }
- ],
- "source": [
- "program = template.create_program(parameters=parameters,\n",
- " channel_mapping={'A': None, 'B': 'Y'},\n",
- " measurement_mapping={'m': 'foo', 'n': None})\n",
- "print(program)\n",
- "print('Defined on', program[0].waveform.defined_channels)\n",
- "print(program.get_measurement_windows())"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Instantiating Composed Pulses\n",
- "\n",
- "Let's have a brief look at a slightly more complex pulse. Say we want to repeat our previous pulse a few times and follow it up with a brief sine wave on each channel."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {
- "scrolled": false
- },
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "LOOP 1 times:\n",
- " ->LOOP 4 times:\n",
- " ->EXEC 1 times\n",
- " ->EXEC 1 times\n",
- "{'m': (array([ 0., 6., 12., 18.]), array([2., 2., 2., 2.])), 'n': (array([ 4., 10., 16., 22.]), array([2., 2., 2., 2.]))}\n"
- ]
- },
- {
- "data": {
- "image/png": "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\n",
- "text/plain": [
- ""
- ]
- },
- "metadata": {
- "needs_background": "light"
- },
- "output_type": "display_data"
- }
- ],
- "source": [
- "from qupulse.pulses import FunctionPT, SequencePT, RepetitionPT, AtomicMultiChannelPT\n",
- "\n",
- "repeated_template = RepetitionPT(template, 'n_rep')\n",
- "sine_template = FunctionPT('sin_a*sin(t)', '2*3.1415')\n",
- "two_channel_sine_template = AtomicMultiChannelPT(\n",
- " (sine_template, {'default': 'A'}), \n",
- " (sine_template, {'default': 'B'}, {'sin_a': 'sin_b'})\n",
- ")\n",
- "sequence_template = SequencePT(repeated_template, two_channel_sine_template)\n",
- "\n",
- "sequence_parameters = dict(parameters) # we just copy our parameter dict from before\n",
- "sequence_parameters['n_rep'] = 4 # and add a few new values for the new params from the sine wave\n",
- "sequence_parameters['sin_a'] = 1\n",
- "sequence_parameters['sin_b'] = 2\n",
- "\n",
- "_ = plot(sequence_template, parameters=sequence_parameters, sample_rate=100, show=False)\n",
- "sequence_program = sequence_template.create_program(parameters=sequence_parameters, \n",
- " channel_mapping={'A': 'A', 'B': 'B'})\n",
- "print(sequence_program)\n",
- "print(sequence_program.get_measurement_windows())"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "As we can see, our `Loop` now contains an inner `Loop` object which repeats a waveform four times and additionally executes another waveform. This reflects the structure of our pulse template. Note also that the single measurement window defined by our pulse template `template` is repeated four times as well in the `Loop` object, according to the number of repetitions of the corresponding pulse.\n",
- "\n",
- "Don't worry too much about the inner workings of the `Loop` objects, though. We were just taking a short look at them here. In practice it will be sufficient to just obtain them using the `create_program` method of `PulseTemplate` and pass them on to `HardwareSetup` when required."
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python [default]",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.7.0"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/doc/source/examples/07MultiChannelTemplates.ipynb b/doc/source/examples/07MultiChannelTemplates.ipynb
deleted file mode 100644
index b0dd1e480..000000000
--- a/doc/source/examples/07MultiChannelTemplates.ipynb
+++ /dev/null
@@ -1,2613 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Multi-Channel Pulses\n",
- "\n",
- "Usually there is a need to define pulses for multiple control channels simulateously. While this would be possible by simply defining several separate pulse templates (one for each channel), qupulse also allows to define pulse templates directly for multiple channels or combine existing templates in a multi-channel way. This tutorial explores these possibilities.\n",
- "\n",
- "## A Multi-Channel Table Pulse\n",
- "`TablePulseTemplate` allows to model multiple channels in a straighforward way: In its constructor entries are given as time-voltage sequences in a dictionary where each key specifies a channel id (which can be an identifier string or a number). In the first few examples we have mostly ignored this but here we are making use of it.\n",
- "\n",
- "The following example constructs a 2-channel table pulse template with shared parameters and plots it."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "The number of channels in table_template is 2.\n"
- ]
- }
- ],
- "source": [
- "from qupulse.pulses import TablePT\n",
- "\n",
- "table_template = TablePT(identifier='2-channel-table-template',\n",
- " entries={'first_channel' : [(0, 0),\n",
- " (1, 4),\n",
- " ('foo', 'bar'),\n",
- " (10, 0)],\n",
- " 'second_channel': [(0, 0),\n",
- " ('foo', 2.7, 'linear'),\n",
- " (9, 'bar', 'linear')]}\n",
- " )\n",
- "\n",
- "# plot it\n",
- "%matplotlib notebook\n",
- "from qupulse.pulses.plotting import plot\n",
- "parameters = dict(\n",
- " foo=7,\n",
- " bar=-1.3\n",
- ")\n",
- "_ = plot(table_template, parameters, sample_rate=100)\n",
- "print(\"The number of channels in table_template is {}.\".format(table_template.num_channels))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Combining Templates: `AtomicMultiChannelPulseTemplate`\n",
- "\n",
- "`AtomicMultiChannelPulseTemplate`(`AtomicMultiChannelPT`) allows to compose a multi-channel template out of atomic (i.e., no control flow) templates of equal duration. It allows to reassign channel indices of the channels of its subtemplates. The constructor is similar to the one of `SequencePulseTemplate` and expects subtemplates (including parameter and channel mappings if required).\n",
- "\n",
- "The following example will combine the two-channel table pulse template `table_template` from above and a function pulse template `function_template` to a three-channel template `template`. We reassign indices such that channel 'rectangle' of the new `template` is channel 'first_channel' and 'triangle' is channel 'second_channel' of `table_template`. Furthermore the parameters get remapped. `function_template` doesn't get changed at all."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "The number of channels in function_template is 1.\n",
- "The number of channels in template is 3.\n"
- ]
- }
- ],
- "source": [
- "from qupulse.pulses import FunctionPT, AtomicMultiChannelPT\n",
- "\n",
- "function_template = FunctionPT('-sin(t)**2', '10', identifier='function-template', channel='wavy')\n",
- "\n",
- "template = AtomicMultiChannelPT(\n",
- " function_template,\n",
- " (table_template, dict(foo='5', bar='2 * hugo'), {'first_channel': 'rectangle ', 'second_channel': 'triangle'}),\n",
- " identifier='3-channel-combined-template'\n",
- ")\n",
- "\n",
- "_ = plot(template, dict(hugo=-1.3), sample_rate=100)\n",
- "print(\"The number of channels in function_template is {}.\".format(function_template.num_channels))\n",
- "print(\"The number of channels in template is {}.\".format(template.num_channels))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The constructor of `AtomicMultiChannelPulseTemplate` expects its subtemplates as positional arguments. Each of positional arguments is required to be either a `AtomicPulseTemplate`, a `MappingPulseTemplate` that wraps an `AtomicPulseTemplate` or a tuple that can be passed to `MappingPulseTemplate.from_tuple`(more examples in [Mapping with the MappingPulseTemplate](05MappingTemplate.ipynb)). The sets of channels on which the subtemplates are defined has to be distinct.\n",
- "Note that an exception will be raised during the sampling of the waveforms (i.e., during the sequencing process) if the subtemplates have different length."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Instantionation duration check\n",
- "By default the AtomicMultiChannelPulseTemplate checks whether the durations are equal on construction. It is possible to do this check during instantiation (when create_program is called) by providing the `duration` keyword argument. This can either be an expression or `True`. If it is an expression all subwaveforms have to have a duration equal to it. If it is `True` all waveforms have to have an unspecified equal duration."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Instantiation duration check:\n",
- "Could not assert duration equality of duration_a and duration_b\n",
- "\n",
- "Instantionation duration check with no specified value:\n",
- "The durations are not all equal. {'a': mpq(3,1), 'b': mpq(4,1)}\n",
- "\n",
- "Instantionation duration check with specified value\n",
- "The duration does not equal the expected duration 4\n"
- ]
- }
- ],
- "source": [
- "from qupulse.pulses import FunctionPT, AtomicMultiChannelPT\n",
- "\n",
- "template_a = FunctionPT('-sin(t)**2', 'duration_a', channel='a')\n",
- "template_b = FunctionPT('-cos(t)**2', 'duration_b', channel='b')\n",
- "\n",
- "try:\n",
- " # instantiation duration check\n",
- " template = AtomicMultiChannelPT(\n",
- " template_a,\n",
- " template_b,\n",
- " identifier='3-channel-combined-template'\n",
- " )\n",
- "except ValueError as err:\n",
- " print('Instantiation duration check:')\n",
- " print(err)\n",
- "\n",
- "# instantionation duration check with no specified value\n",
- "template_unspecified = AtomicMultiChannelPT(\n",
- " template_a,\n",
- " template_b,\n",
- " duration=True\n",
- ")\n",
- "\n",
- "template_unspecified.create_program(parameters=dict(duration_a=3, duration_b=3))\n",
- "try:\n",
- " template_unspecified.create_program(parameters=dict(duration_a=3, duration_b=4))\n",
- "except ValueError as err:\n",
- " print()\n",
- " print('Instantionation duration check with no specified value:')\n",
- " print(err.args[0], err.args[1])\n",
- "\n",
- "\n",
- "# instantionation duration check with specified value\n",
- "template_specified = AtomicMultiChannelPT(\n",
- " template_a,\n",
- " template_b,\n",
- " duration='my_duration'\n",
- ")\n",
- "template_specified.create_program(parameters=dict(duration_a=3, duration_b=3, my_duration=3))\n",
- "try:\n",
- " template_specified.create_program(parameters=dict(duration_a=3, duration_b=3, my_duration=4))\n",
- "except ValueError as err:\n",
- " print()\n",
- " print('Instantionation duration check with specified value')\n",
- " print(err.args[0], err.args[1])\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Multiple Channels in Non-Atomic Templates\n",
- "\n",
- "All higher order template, i.e., `SequencePulseTemplate` and `ForLoopPulseTemplate` and `RepetitionPulseTemplate`, also support multiple channels insofar as that they can be composed using multi-channel atomic templates as subtemplates. They require that all these subtemplates define the same channels and raise an exception if that is not the case. The following example constructs a `SequencePulseTempate` `sequence_template` by chaining the above defined two-channel `table_template`. In the second instance of `table_template` in the sequence, we swap the channels by wrapping a `MappingPulseTemplate` around it."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "The number of channels in sequence_template is 2.\n"
- ]
- }
- ],
- "source": [
- "from qupulse.pulses import SequencePT\n",
- "\n",
- "sequence_template = SequencePT(\n",
- " (table_template, dict(foo='1.2 * hugo', bar='hugo ** 2')),\n",
- " (table_template, dict(foo='1.2 * hugo', bar='hugo ** 2'), {'first_channel': 'second_channel', \n",
- " 'second_channel': 'first_channel'}),\n",
- " identifier='2-channel-sequence-template'\n",
- ")\n",
- "\n",
- "plot(sequence_template, dict(hugo=2), sample_rate=100)\n",
- "print(\"The number of channels in sequence_template is {}.\".format(sequence_template.num_channels))"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python [default]",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.7.0"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 1
-}
diff --git a/doc/source/examples/09ParameterConstraints.ipynb b/doc/source/examples/09ParameterConstraints.ipynb
deleted file mode 100644
index 95173d4fb..000000000
--- a/doc/source/examples/09ParameterConstraints.ipynb
+++ /dev/null
@@ -1,928 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Constraining Parameters\n",
- "\n",
- "Often, it is useful to constrain parameters. Either to be in a specific range or even that some relation between parameters is fulfilled. Many pulse templates allow that and accept `parameter_constraints` as a keyword argument. In this example we look at a simple table pulse that ramps a voltage from `v_a` to `v_b` with the ramp time `t_ramp`."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "%matplotlib notebook\n",
- "from qupulse.pulses import TablePT\n",
- "from qupulse.pulses.plotting import plot\n",
- "\n",
- "table_pulse = TablePT({'A': [(0, 'v_a'),\n",
- " ('t_ramp', 'v_b', 'linear')]})\n",
- "_ = plot(table_pulse, dict(t_ramp=10, v_a=-1, v_b=1), sample_rate=100)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Now, we want to restrict the ramp rate of the pulse to some maximum ramp rate `max_rate` and the ramp time to be larger than 1:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{'v_a', 'max_rate', 't_ramp', 'v_b'}\n",
- "Abs(v_a - v_b)/t_ramp < max_rate\n",
- "t_ramp > 1\n"
- ]
- }
- ],
- "source": [
- "table_pulse = TablePT({'A': [(0, 'v_a'),\n",
- " ('t_ramp', 'v_b', 'linear')]},\n",
- " parameter_constraints=['Abs(v_a-v_b)/t_ramp < max_rate', 't_ramp>1'])\n",
- "print(table_pulse.parameter_names)\n",
- "print(table_pulse.parameter_constraints[0])\n",
- "print(table_pulse.parameter_constraints[1])"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We see that the pulse got the extra parameter `max_rate`. We cannot instantiate this pulse without providing this parameter."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "ParameterNotProvidedException: No value was provided for parameter ''max_rate''.\n"
- ]
- }
- ],
- "source": [
- "try:\n",
- " _ = plot(table_pulse, dict(t_ramp=10, v_a=-1, v_b=1), sample_rate=100)\n",
- "except Exception as exception:\n",
- " print('{}: {}'.format(type(exception).__name__, exception))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "If one of the constraints is violated an exception is raised:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "ParameterConstraintViolation: The constraint 'Abs(v_a - v_b)/t_ramp < max_rate' is not fulfilled.\n",
- "Parameters: {'v_a': -1, 'max_rate': 0.1, 't_ramp': 10, 'v_b': 1}\n"
- ]
- }
- ],
- "source": [
- "try:\n",
- " _ = plot(table_pulse, dict(t_ramp=10, v_a=-1, v_b=1, max_rate=0.1), sample_rate=100)\n",
- "except Exception as exception:\n",
- " print('{}: {}'.format(type(exception).__name__, exception))"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python [default]",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.7.0"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/doc/source/examples/10FreeInductionDecayExample.ipynb b/doc/source/examples/10FreeInductionDecayExample.ipynb
deleted file mode 100644
index f201becfc..000000000
--- a/doc/source/examples/10FreeInductionDecayExample.ipynb
+++ /dev/null
@@ -1,1817 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Free Induction Decay - A Real Use Case\n",
- "\n",
- "The following will give an example of a complex pulse using many of the features discussed in the previous tutorial examles: We will use two channels, parameters and parameter constraints, parameterized measurements and atomic and non-atomic pulse templates. This is based on real experiments. To see another, a bit more artificial example for a pulse setup use case that offers more verbose explanations, see [Gate Configuration - A Full Use Case](11GateConfigurationExample.ipynb).\n",
- "\n",
- "We start by creating some atomic pulse templates using `PointPT` which will be the building blocks for the more complex pulse structure we have in mind."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [],
- "source": [
- "from qupulse.pulses import PointPT, SequencePT, ForLoopPT, RepetitionPT, MappingPT\n",
- "import qupulse.pulses.plotting\n",
- "import numpy as np\n",
- "import sympy as sp\n",
- "from sympy import sympify as S\n",
- "\n",
- "channel_names = ['RFX', 'RFY']\n",
- "\n",
- "S_init = PointPT([(0, 'S_init'),\n",
- " ('t_init', 'S_init')],\n",
- " channel_names=channel_names, identifier='S_init')\n",
- "\n",
- "meas_wait = PointPT([(0, 'meas'),\n",
- " ('t_meas_wait', 'meas')],\n",
- " channel_names=channel_names)\n",
- "\n",
- "adprep = PointPT([(0, 'meas'),\n",
- " ('t_ST_prep', 'ST_plus - ST_jump/2', 'linear'),\n",
- " ('t_ST_prep', 'ST_plus + ST_jump/2'),\n",
- " ('t_op', 'op', 'linear')],\n",
- " parameter_constraints=['Abs(ST_plus - ST_jump/2 - meas) <= Abs(ST_plus - meas)',\n",
- " 'Abs(ST_plus - ST_jump/2 - meas)/t_ST_prep <= max_ramp_speed',\n",
- " 'Abs(ST_plus + ST_jump/2 - op)/Abs(t_ST_prep-t_op) <= max_ramp_speed'],\n",
- " channel_names=channel_names, identifier='adprep')\n",
- "\n",
- "adread = PointPT([(0, 'op'),\n",
- " ('t_ST_read', 'ST_plus + ST_jump/2', 'linear'),\n",
- " ('t_ST_read', 'ST_plus - ST_jump/2'),\n",
- " ('t_meas_start', 'meas', 'linear'),\n",
- " ('t_meas_start + t_meas_duration', 'meas')],\n",
- " parameter_constraints=['Abs(ST_plus - ST_jump/2 - meas) <= Abs(ST_plus - meas)',\n",
- " 'Abs(ST_plus - ST_jump/2 - meas)/t_ST_read <= max_ramp_speed',\n",
- " 'Abs(ST_plus + ST_jump/2 - op)/Abs(t_ST_read-t_op) <= max_ramp_speed'],\n",
- " channel_names=channel_names, identifier='adread',\n",
- " measurements=[('m', 't_meas_start', 't_meas_duration')])\n",
- "\n",
- "free_induction = PointPT([(0, 'op-eps_J'),\n",
- " ('t_fid', 'op-eps_J')], channel_names=channel_names)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "In the next step, we combine our building blocks into more complex pulses step by step.\n",
- "We first define our core functionality pulse template `stepped_free_induction`.\n",
- "The pulse template `pulse` surrounds our functionality with pulses to reset/initialize our qubit and allow for data acquisition.\n",
- "We will use `pulse` in a `ForLoopPT` `looped_pulse` to perform a parameter sweep. Our final pulse template `experiment` repeats this whole thing a number of times to allow for statistical aggregating of measurement data and represents the complete pulse template for our experiment."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {},
- "outputs": [],
- "source": [
- "\n",
- "\n",
- "stepped_free_induction = MappingPT(free_induction, parameter_mapping={'t_fid': 't_start + i_fid*t_step'}, allow_partial_parameter_mapping=True)\n",
- "\n",
- "pulse = SequencePT(S_init, meas_wait, adprep, stepped_free_induction, adread)\n",
- "\n",
- "looped_pulse = ForLoopPT(pulse, loop_index='i_fid', loop_range='N_fid_steps')\n",
- "\n",
- "experiment = RepetitionPT(looped_pulse, 'N_repetitions', identifier='free_induction_decay')"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{'max_ramp_speed', 't_meas_start', 'ST_jump', 't_ST_read', 'eps_J', 't_init', 'ST_plus', 'N_repetitions', 't_step', 't_start', 'op', 't_meas_duration', 'S_init', 't_meas_wait', 'N_fid_steps', 't_op', 'meas', 't_ST_prep'}\n"
- ]
- }
- ],
- "source": [
- "print(experiment.parameter_names)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let's use some reasonable (but low) values for our parameters and plot our `experiment` pulse (we set the number of repeititions of `looped_pulse` only to 2 so that the plot does not get too stuffed).\n",
- "\n",
- "Note that we provide numpy arrays of length 2 for some parameters to assign different values for different channels (see also [The PointPulseTemplate](03PointPulse.ipynb))."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "%matplotlib notebook\n",
- "\n",
- "example_values = dict(meas=[0, 0],\n",
- " op=[5, -5],\n",
- " eps_J=[1, -1],\n",
- " ST_plus=[2.5, -2.5],\n",
- " S_init=[-1, -1],\n",
- " ST_jump=[1, -1],\n",
- " max_ramp_speed=0.3,\n",
- " \n",
- " t_init=5,\n",
- " \n",
- " t_meas_wait = 1,\n",
- " \n",
- " t_ST_prep = 10,\n",
- " t_op = 20,\n",
- " \n",
- " t_ST_read = 10,\n",
- " t_meas_start = 20,\n",
- " t_meas_duration=5,\n",
- " \n",
- " t_start=0,\n",
- " t_step=5,\n",
- " N_fid_steps=5, N_repetitions=2)\n",
- "\n",
- "from qupulse.pulses.plotting import plot\n",
- "\n",
- "_ = plot(experiment, example_values)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We can clearly make out the many repetitions of our basic functionality pulse and also the varying duration between the voltage peaks due to our parameter sweep (as well as the two-fold repetition of the sweep itself).\n",
- "\n",
- "Let's also quickly plot only a single repetition by setting according parameters for our `experiment` pulse template."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "example_values['N_fid_steps'] = 1\n",
- "example_values['N_repetitions'] = 1\n",
- "example_values['t_start'] = 5\n",
- "\n",
- "_ = plot(experiment, example_values)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "As a last step we will save the pulse and some example parameters so we can use it in other examples."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 6,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Successfully saved pulse and example parameters\n"
- ]
- }
- ],
- "source": [
- "import json\n",
- "from qupulse.serialization import FilesystemBackend, PulseStorage\n",
- "\n",
- "pulse_storage = PulseStorage(FilesystemBackend('./serialized_pulses'))\n",
- "\n",
- "# overwrite all pulses explicitly\n",
- "pulse_storage.overwrite('adprep', adprep)\n",
- "pulse_storage.overwrite('S_init', S_init)\n",
- "pulse_storage.overwrite('adread', adread)\n",
- "pulse_storage.overwrite('free_induction_decay', experiment)\n",
- "\n",
- "with open('parameters/free_induction_decay.json', 'w') as parameter_file:\n",
- " json.dump(example_values, parameter_file)\n",
- "\n",
- "print('Successfully saved pulse and example parameters')"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python [default]",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.7.0"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/doc/source/examples/11GateConfigurationExample.ipynb b/doc/source/examples/11GateConfigurationExample.ipynb
deleted file mode 100644
index d59ded922..000000000
--- a/doc/source/examples/11GateConfigurationExample.ipynb
+++ /dev/null
@@ -1,4261 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Gate Configuration - A Full Use Case"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "collapsed": true
- },
- "source": [
- "An example for a real use case of qupulse is the search for and evaluation of parameters for pulses that represent quantum gate operations on a toy example. To see an example closer to reality but less verbose in explanations, please see [Free Induction Decay - A Real Use Case](10FreeInductionDecayExample.ipynb).\n",
- "\n",
- "## Description of the Experiment\n",
- "The experiment will typically involve a set of gate pulses $G_j, 0 \\leq j \\lt N_{Gates}$.\n",
- "\n",
- "The template for a gate pulse $G_j$ is a sequence of $\\epsilon_i, 0 \\leq i \\lt N_{G_j}$ voltage levels held for time $\\Delta t = 1$ ns as illustrated in the figure below (with $N_{G_j} = 7$).\n",
- "\n",
- "\n",
- "\n",
- "The experiment defines a number of sequences $S_k, 0 \\leq k \\lt N_{Sequences}$ of the $G_j$ as $$S_k = (G_{m_k(1)}, G_{m_k(2)}, \\dots, G_{m_k(N_{S_k})})$$ where $N_{S_k}$ is the length of sequence $k$ and $m_k(i): \\{0, \\dots, N_{S_k} - 1\\} \\rightarrow \\{0, \\dots, N_{Gates} - 1\\}$ is a function that maps an index $i$ to the $m_k(i)$-th gate of sequence $S_k$ and thus fully describes the sequence. (These sequences express the sequential application of the gates to the qubit. In terms of quantum mathematics they may rather be expressed as multiplication of the matrices describing the unitary transformations applied by the gates: $S_k = \\prod_{i=N_{S_k} - 1}^{0} G_{m_k(i)} = G_{(N_{S_k} - 1)} \\cdot \\dots \\cdot G_{1} \\cdot G_{0}$.)\n",
- "\n",
- "Measuring and analysing the effects of these sequences on the qubit's state to derive parameters $\\epsilon_i$ for gate pulses that achieve certain state transformations is the goal of the experiment.\n",
- "\n",
- "To this end, every sequence must be extended by some preceeding initialization pulse and a succeeding measurement pulse. Furthermore, due to hardware constraints in measuring, all sequences must be of equal length (which is typically 4 µs). Thus, some sequences require some wait time before initialization to increase their playback duration. These requirements give raise to extended sequences $S_k'$ of the form:\n",
- "$$S_k' = I_{p(k)} | W_k | S_k | M_{q(k)}$$\n",
- "where the functions $p(k)$ and $q(k)$ respectively select some initialization pulse $I_{p(k)} \\in \\{I_1, I_2, \\dots\\}$ and measurement pulse $M_{q(k)} \\in \\{M_1, M_2, \\dots\\}$ for sequence $k$ and $W_k$ is the aforementioned wait pulse. The '|' denote concatenation of pulses, i.e., sequential execution.\n",
- "\n",
- "Since measurement of quantum state is a probabilistic process, many measurements of the effect of a single sequence must be made to reconstruct the resulting state of the qubit. Thus, the experiment at last defines scanlines (typically of duration 1 second), which are sequences of the $S_k'$. (These simply represent batches of sequences to configure playback and measurement systems and have no meaning to the experiment beyond these practical considerations.)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Implementation Using qupulse\n",
- "\n",
- "We now want to illustrate how to setup the experiment described above using qupulse. Let us assume the experiment considers only two different gates pulses ($N_{Gates} = 2$). We further assume that $N_{G_1} = 20$ and $N_{G_2} = 18$. We define them using instances of `TablePulseTemplate`:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [],
- "source": [
- "from qupulse.pulses import TablePT\n",
- "\n",
- "delta_t = 1 # assuming that delta_t is 1 elementary time unit long in pulse defintions, i.e. 1 time unit = 1 ns\n",
- "\n",
- "gate_0 = TablePT({0: [(i*delta_t, 'gate_0_eps_' + str(i))\n",
- " for i in range(19)]})\n",
- " \n",
- "gate_1 = TablePT({0: [(i*delta_t, 'gate_2_eps_' + str(i))\n",
- " for i in range(17)]})\n",
- " \n",
- "gates = [gate_0, gate_1]"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We thus obtain two `TablePulseTemplate` of the desired form with parameters 'gate_1_eps_1' to 'gate_1_eps_20' and 'gate_2_eps_1' to 'gate_2_eps_18'.\n",
- "\n",
- "Next, we will define sequences as `SequncePulseTemplate` objects. We assume that the mapping functions $m_k$ are given as a 2 dimensional array (which impilicitly also defines $N_{Sequences}$ and $N_{S_k}$), e.g.:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {},
- "outputs": [],
- "source": [
- "m = [\n",
- " [0, 1, 0, 0, 0, 1, 1, 0, 1], # m_0(i)\n",
- " [1, 1, 0, 0, 1, 0], # m_1(i)\n",
- " [1, 0, 0, 1, 1, 0, 0, 1] #m_2(i)\n",
- " ]"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The `SequencePulseTemplate` objects can now easily be constructed:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "metadata": {},
- "outputs": [],
- "source": [
- "from qupulse.pulses import SequencePT\n",
- "\n",
- "# SequencePulseTemplate requires a definition of parameter mappings from parameters passed into the\n",
- "# SequencePulseTemplate object to its subtemplates. In this case, we want parameters to map 1 to 1\n",
- "# and thus create an identity mapping of parameter names for both gates using python list/set/dict comprehension\n",
- "epsilon_mappings = [\n",
- " {param_name: param_name for param_name in gates[0].parameter_names},\n",
- " {param_name: param_name for param_name in gates[1].parameter_names}\n",
- "]\n",
- "all_epsilons = gates[0].parameter_names | gates[1].parameter_names\n",
- "\n",
- "sequences = []\n",
- "for m_k in m:\n",
- " subtemplates = []\n",
- " \n",
- " sequences.append(SequencePT(*(gates[g_ki] for g_ki in m_k)))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We end up with a list `sequences` which contains the sequences described by our $m$ as qupulse pulse templates.\n",
- "\n",
- "To visualize our progress, let us plot our two gates and the second sequence with some random values between $-5$ and $5$ for the $\\epsilon_i$:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {
- "scrolled": false
- },
- "outputs": [
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "import random\n",
- "\n",
- "random.seed('Some seed such that numbers generated are predictable')\n",
- "parameters = {parameter_name: random.random() * 10 - 5 for parameter_name in all_epsilons}\n",
- "\n",
- "%matplotlib notebook\n",
- "from qupulse.pulses.plotting import plot\n",
- "_ = plot(gates[0], parameters)\n",
- "_ = plot(gates[1], parameters)\n",
- "_ = plot(sequences[1], parameters)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We now must construct the $S_k'$. For simplicity, we assume that there is only one initialization and one measurement pulse which are defined somehow and define only stubs here. We also define a waiting pulse with a variable length:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "metadata": {},
- "outputs": [],
- "source": [
- "# stub for an initialization pulse of length 4\n",
- "init = TablePT({0: [(0, 5), (4, 0, 'linear')]})\n",
- "\n",
- "# stub for a measurement pulse of length 12\n",
- "measure = TablePT({0: [(0, 0), (12, 5, 'linear')]})\n",
- "\n",
- "# a wating pulse\n",
- "wait = TablePT({0: [(0, 0), ('wait_duration', 0)]})"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "For our example, let us assume that we want all $S_k'$ to take 200 ns (since we've chosen the $S_k$ to be rather short). We know that the duration of our gate pulses in nanoseconds is equal to the number of entries in the `TablePulseTemplate` objects (each voltage level is held for one unit of time in the tables which corresponds to $\\Delta t = 1$ ns by convention). Accordingly, the init pulse lasts for 4 ns and the measure pulse for 12 ns. The required length of the wait pulse can then be computed as follows:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 6,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "[175, 178, 176]\n"
- ]
- }
- ],
- "source": [
- "wait_times = []\n",
- "desired_time = 200\n",
- "\n",
- "for m_k in m:\n",
- " duration_k = 4 + 12 # init + measurement duration\n",
- " for g_ki in m_k:\n",
- " duration_k += len(gates[g_ki].entries) # add the number of entries of all gates in the sequence\n",
- " wait_time_k = desired_time - duration_k\n",
- " wait_times.append(wait_time_k)\n",
- " \n",
- "print(wait_times)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Finally we can construct the $S_k'$:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 7,
- "metadata": {},
- "outputs": [],
- "source": [
- "# an identity mapping for all epsilons\n",
- "all_epsilons_map = {param_name: param_name for param_name in all_epsilons}\n",
- "\n",
- "gates_with_init_and_readout = [SequencePT((wait, {'wait_duration': wait_duration}),\n",
- " init,\n",
- " gate,\n",
- " measure)\n",
- " for gate, wait_duration in zip(sequences, wait_times)]\n",
- "final_sequence = SequencePT(*gates_with_init_and_readout)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let us plot $S_1'$ to see whether we've accomplished our goal:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 8,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "pl = plot(final_sequence.subtemplates[1], parameters)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let us also plot the whole sequence $S_1' | S_2' | S_3'$ as well"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 9,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "_ = plot(final_sequence, parameters)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Finally, we construct a single scanline which just repeats all three sequences over and over again. Since our $S_k'$ are short, we just build a scanline with a duration of 0.6 microseconds. With a duration for each $S_k'$ of 200 ns, we can fit 1'000 repetitions of $S_1' | S_2' | S_3'$ in our scanline. We will, however, not plot our scanline because it will be quite large despite it's short duration."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 10,
- "metadata": {},
- "outputs": [],
- "source": [
- "from qupulse.pulses import RepetitionPT\n",
- "scanline = RepetitionPT(final_sequence, 1000)"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python [default]",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.7.0"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 1
-}
diff --git a/doc/source/examples/13RetrospectiveConstantChannelAddition.ipynb b/doc/source/examples/13RetrospectiveConstantChannelAddition.ipynb
deleted file mode 100644
index ad941c8c0..000000000
--- a/doc/source/examples/13RetrospectiveConstantChannelAddition.ipynb
+++ /dev/null
@@ -1,926 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# ParallelConstantChannelPulseTemplate\n",
- "One reoccuring problem is to add a constant channel to an already existing possibly complex pulse. The setting in this example requires us to put a trigger pulse before the example pulse written in [10FreeInductionDecayExample](10FreeInductionDecayExample.ipynb). Unfortunately, the trigger pulse has to be played on a seperate marker channel that is not included in the example pulse. Therefore we will add this channel to the pulse with the constant value 0.\n",
- "\n",
- "Let us start with loading the experiment and defining the trigger pulse"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Defined channels of loaded pulse: {'RFY', 'RFX'}\n",
- "Defined channels of trigger pulse: {'RFY', 'Marker', 'RFX'}\n"
- ]
- }
- ],
- "source": [
- "from qupulse.pulses import TablePT\n",
- "from qupulse.serialization import FilesystemBackend, PulseStorage\n",
- "\n",
- "pulse_storage = PulseStorage(FilesystemBackend('./serialized_pulses'))\n",
- "free_induction_decay = pulse_storage['free_induction_decay']\n",
- "print('Defined channels of loaded pulse:', free_induction_decay.defined_channels)\n",
- "\n",
- "trig_pulse = TablePT({'RFX': [(0, 0), ('t_trig', 0)],\n",
- " 'RFY': [(0, 0), ('t_trig', 0)],\n",
- " 'Marker': [(0, 1), ('t_trig', 1)]})\n",
- "print('Defined channels of trigger pulse:', trig_pulse.defined_channels)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "If we now try to concatenate the pulses we get an error as they differ in their defined channels."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "ValueError('The subtemplates are defined for different channels')\n"
- ]
- }
- ],
- "source": [
- "try:\n",
- " experiment = trig_pulse @ free_induction_decay\n",
- "except ValueError as err:\n",
- " print(repr(err))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We can now add an extra channel with a constant value to the `free_induction_decay` pulse. This allows us to concatenate the pulses."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "metadata": {},
- "outputs": [],
- "source": [
- "from qupulse.pulses.multi_channel_pulse_template import ParallelConstantChannelPulseTemplate\n",
- "extended_free_induction_decay = ParallelConstantChannelPulseTemplate(free_induction_decay, {'Marker': 0})\n",
- "\n",
- "experiment = trig_pulse @ extended_free_induction_decay"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Read example parameters from file and plot complete pulse"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "application/javascript": [
- "/* Put everything inside the global mpl namespace */\n",
- "window.mpl = {};\n",
- "\n",
- "\n",
- "mpl.get_websocket_type = function() {\n",
- " if (typeof(WebSocket) !== 'undefined') {\n",
- " return WebSocket;\n",
- " } else if (typeof(MozWebSocket) !== 'undefined') {\n",
- " return MozWebSocket;\n",
- " } else {\n",
- " alert('Your browser does not have WebSocket support.' +\n",
- " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
- " 'Firefox 4 and 5 are also supported but you ' +\n",
- " 'have to enable WebSockets in about:config.');\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n",
- " this.id = figure_id;\n",
- "\n",
- " this.ws = websocket;\n",
- "\n",
- " this.supports_binary = (this.ws.binaryType != undefined);\n",
- "\n",
- " if (!this.supports_binary) {\n",
- " var warnings = document.getElementById(\"mpl-warnings\");\n",
- " if (warnings) {\n",
- " warnings.style.display = 'block';\n",
- " warnings.textContent = (\n",
- " \"This browser does not support binary websocket messages. \" +\n",
- " \"Performance may be slow.\");\n",
- " }\n",
- " }\n",
- "\n",
- " this.imageObj = new Image();\n",
- "\n",
- " this.context = undefined;\n",
- " this.message = undefined;\n",
- " this.canvas = undefined;\n",
- " this.rubberband_canvas = undefined;\n",
- " this.rubberband_context = undefined;\n",
- " this.format_dropdown = undefined;\n",
- "\n",
- " this.image_mode = 'full';\n",
- "\n",
- " this.root = $('');\n",
- " this._root_extra_style(this.root)\n",
- " this.root.attr('style', 'display: inline-block');\n",
- "\n",
- " $(parent_element).append(this.root);\n",
- "\n",
- " this._init_header(this);\n",
- " this._init_canvas(this);\n",
- " this._init_toolbar(this);\n",
- "\n",
- " var fig = this;\n",
- "\n",
- " this.waiting = false;\n",
- "\n",
- " this.ws.onopen = function () {\n",
- " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n",
- " fig.send_message(\"send_image_mode\", {});\n",
- " if (mpl.ratio != 1) {\n",
- " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n",
- " }\n",
- " fig.send_message(\"refresh\", {});\n",
- " }\n",
- "\n",
- " this.imageObj.onload = function() {\n",
- " if (fig.image_mode == 'full') {\n",
- " // Full images could contain transparency (where diff images\n",
- " // almost always do), so we need to clear the canvas so that\n",
- " // there is no ghosting.\n",
- " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
- " }\n",
- " fig.context.drawImage(fig.imageObj, 0, 0);\n",
- " };\n",
- "\n",
- " this.imageObj.onunload = function() {\n",
- " fig.ws.close();\n",
- " }\n",
- "\n",
- " this.ws.onmessage = this._make_on_message_function(this);\n",
- "\n",
- " this.ondownload = ondownload;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_header = function() {\n",
- " var titlebar = $(\n",
- " '');\n",
- " var titletext = $(\n",
- " '');\n",
- " titlebar.append(titletext)\n",
- " this.root.append(titlebar);\n",
- " this.header = titletext[0];\n",
- "}\n",
- "\n",
- "\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_canvas = function() {\n",
- " var fig = this;\n",
- "\n",
- " var canvas_div = $('');\n",
- "\n",
- " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n",
- "\n",
- " function canvas_keyboard_event(event) {\n",
- " return fig.key_event(event, event['data']);\n",
- " }\n",
- "\n",
- " canvas_div.keydown('key_press', canvas_keyboard_event);\n",
- " canvas_div.keyup('key_release', canvas_keyboard_event);\n",
- " this.canvas_div = canvas_div\n",
- " this._canvas_extra_style(canvas_div)\n",
- " this.root.append(canvas_div);\n",
- "\n",
- " var canvas = $('');\n",
- " canvas.addClass('mpl-canvas');\n",
- " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n",
- "\n",
- " this.canvas = canvas[0];\n",
- " this.context = canvas[0].getContext(\"2d\");\n",
- "\n",
- " var backingStore = this.context.backingStorePixelRatio ||\n",
- "\tthis.context.webkitBackingStorePixelRatio ||\n",
- "\tthis.context.mozBackingStorePixelRatio ||\n",
- "\tthis.context.msBackingStorePixelRatio ||\n",
- "\tthis.context.oBackingStorePixelRatio ||\n",
- "\tthis.context.backingStorePixelRatio || 1;\n",
- "\n",
- " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
- "\n",
- " var rubberband = $('');\n",
- " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n",
- "\n",
- " var pass_mouse_events = true;\n",
- "\n",
- " canvas_div.resizable({\n",
- " start: function(event, ui) {\n",
- " pass_mouse_events = false;\n",
- " },\n",
- " resize: function(event, ui) {\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " stop: function(event, ui) {\n",
- " pass_mouse_events = true;\n",
- " fig.request_resize(ui.size.width, ui.size.height);\n",
- " },\n",
- " });\n",
- "\n",
- " function mouse_event_fn(event) {\n",
- " if (pass_mouse_events)\n",
- " return fig.mouse_event(event, event['data']);\n",
- " }\n",
- "\n",
- " rubberband.mousedown('button_press', mouse_event_fn);\n",
- " rubberband.mouseup('button_release', mouse_event_fn);\n",
- " // Throttle sequential mouse events to 1 every 20ms.\n",
- " rubberband.mousemove('motion_notify', mouse_event_fn);\n",
- "\n",
- " rubberband.mouseenter('figure_enter', mouse_event_fn);\n",
- " rubberband.mouseleave('figure_leave', mouse_event_fn);\n",
- "\n",
- " canvas_div.on(\"wheel\", function (event) {\n",
- " event = event.originalEvent;\n",
- " event['data'] = 'scroll'\n",
- " if (event.deltaY < 0) {\n",
- " event.step = 1;\n",
- " } else {\n",
- " event.step = -1;\n",
- " }\n",
- " mouse_event_fn(event);\n",
- " });\n",
- "\n",
- " canvas_div.append(canvas);\n",
- " canvas_div.append(rubberband);\n",
- "\n",
- " this.rubberband = rubberband;\n",
- " this.rubberband_canvas = rubberband[0];\n",
- " this.rubberband_context = rubberband[0].getContext(\"2d\");\n",
- " this.rubberband_context.strokeStyle = \"#000000\";\n",
- "\n",
- " this._resize_canvas = function(width, height) {\n",
- " // Keep the size of the canvas, canvas container, and rubber band\n",
- " // canvas in synch.\n",
- " canvas_div.css('width', width)\n",
- " canvas_div.css('height', height)\n",
- "\n",
- " canvas.attr('width', width * mpl.ratio);\n",
- " canvas.attr('height', height * mpl.ratio);\n",
- " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n",
- "\n",
- " rubberband.attr('width', width);\n",
- " rubberband.attr('height', height);\n",
- " }\n",
- "\n",
- " // Set the figure to an initial 600x600px, this will subsequently be updated\n",
- " // upon first draw.\n",
- " this._resize_canvas(600, 600);\n",
- "\n",
- " // Disable right mouse context menu.\n",
- " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n",
- " return false;\n",
- " });\n",
- "\n",
- " function set_focus () {\n",
- " canvas.focus();\n",
- " canvas_div.focus();\n",
- " }\n",
- "\n",
- " window.setTimeout(set_focus, 100);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items) {\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) {\n",
- " // put a spacer in here.\n",
- " continue;\n",
- " }\n",
- " var button = $('');\n",
- " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
- " 'ui-button-icon-only');\n",
- " button.attr('role', 'button');\n",
- " button.attr('aria-disabled', 'false');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- "\n",
- " var icon_img = $('');\n",
- " icon_img.addClass('ui-button-icon-primary ui-icon');\n",
- " icon_img.addClass(image);\n",
- " icon_img.addClass('ui-corner-all');\n",
- "\n",
- " var tooltip_span = $('');\n",
- " tooltip_span.addClass('ui-button-text');\n",
- " tooltip_span.html(tooltip);\n",
- "\n",
- " button.append(icon_img);\n",
- " button.append(tooltip_span);\n",
- "\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " var fmt_picker_span = $('');\n",
- "\n",
- " var fmt_picker = $('');\n",
- " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
- " fmt_picker_span.append(fmt_picker);\n",
- " nav_element.append(fmt_picker_span);\n",
- " this.format_dropdown = fmt_picker[0];\n",
- "\n",
- " for (var ind in mpl.extensions) {\n",
- " var fmt = mpl.extensions[ind];\n",
- " var option = $(\n",
- " '', {selected: fmt === mpl.default_extension}).html(fmt);\n",
- " fmt_picker.append(option)\n",
- " }\n",
- "\n",
- " // Add hover states to the ui-buttons\n",
- " $( \".ui-button\" ).hover(\n",
- " function() { $(this).addClass(\"ui-state-hover\");},\n",
- " function() { $(this).removeClass(\"ui-state-hover\");}\n",
- " );\n",
- "\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {\n",
- " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
- " // which will in turn request a refresh of the image.\n",
- " this.send_message('resize', {'width': x_pixels, 'height': y_pixels});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_message = function(type, properties) {\n",
- " properties['type'] = type;\n",
- " properties['figure_id'] = this.id;\n",
- " this.ws.send(JSON.stringify(properties));\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.send_draw_message = function() {\n",
- " if (!this.waiting) {\n",
- " this.waiting = true;\n",
- " this.ws.send(JSON.stringify({type: \"draw\", figure_id: this.id}));\n",
- " }\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " var format_dropdown = fig.format_dropdown;\n",
- " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
- " fig.ondownload(fig, format);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.figure.prototype.handle_resize = function(fig, msg) {\n",
- " var size = msg['size'];\n",
- " if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {\n",
- " fig._resize_canvas(size[0], size[1]);\n",
- " fig.send_message(\"refresh\", {});\n",
- " };\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
- " var x0 = msg['x0'] / mpl.ratio;\n",
- " var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;\n",
- " var x1 = msg['x1'] / mpl.ratio;\n",
- " var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;\n",
- " x0 = Math.floor(x0) + 0.5;\n",
- " y0 = Math.floor(y0) + 0.5;\n",
- " x1 = Math.floor(x1) + 0.5;\n",
- " y1 = Math.floor(y1) + 0.5;\n",
- " var min_x = Math.min(x0, x1);\n",
- " var min_y = Math.min(y0, y1);\n",
- " var width = Math.abs(x1 - x0);\n",
- " var height = Math.abs(y1 - y0);\n",
- "\n",
- " fig.rubberband_context.clearRect(\n",
- " 0, 0, fig.canvas.width, fig.canvas.height);\n",
- "\n",
- " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_figure_label = function(fig, msg) {\n",
- " // Updates the figure title.\n",
- " fig.header.textContent = msg['label'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_cursor = function(fig, msg) {\n",
- " var cursor = msg['cursor'];\n",
- " switch(cursor)\n",
- " {\n",
- " case 0:\n",
- " cursor = 'pointer';\n",
- " break;\n",
- " case 1:\n",
- " cursor = 'default';\n",
- " break;\n",
- " case 2:\n",
- " cursor = 'crosshair';\n",
- " break;\n",
- " case 3:\n",
- " cursor = 'move';\n",
- " break;\n",
- " }\n",
- " fig.rubberband_canvas.style.cursor = cursor;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_message = function(fig, msg) {\n",
- " fig.message.textContent = msg['message'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_draw = function(fig, msg) {\n",
- " // Request the server to send over a new figure.\n",
- " fig.send_draw_message();\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_image_mode = function(fig, msg) {\n",
- " fig.image_mode = msg['mode'];\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Called whenever the canvas gets updated.\n",
- " this.send_message(\"ack\", {});\n",
- "}\n",
- "\n",
- "// A function to construct a web socket function for onmessage handling.\n",
- "// Called in the figure constructor.\n",
- "mpl.figure.prototype._make_on_message_function = function(fig) {\n",
- " return function socket_on_message(evt) {\n",
- " if (evt.data instanceof Blob) {\n",
- " /* FIXME: We get \"Resource interpreted as Image but\n",
- " * transferred with MIME type text/plain:\" errors on\n",
- " * Chrome. But how to set the MIME type? It doesn't seem\n",
- " * to be part of the websocket stream */\n",
- " evt.data.type = \"image/png\";\n",
- "\n",
- " /* Free the memory for the previous frames */\n",
- " if (fig.imageObj.src) {\n",
- " (window.URL || window.webkitURL).revokeObjectURL(\n",
- " fig.imageObj.src);\n",
- " }\n",
- "\n",
- " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
- " evt.data);\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- " else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\n",
- " fig.imageObj.src = evt.data;\n",
- " fig.updated_canvas_event();\n",
- " fig.waiting = false;\n",
- " return;\n",
- " }\n",
- "\n",
- " var msg = JSON.parse(evt.data);\n",
- " var msg_type = msg['type'];\n",
- "\n",
- " // Call the \"handle_{type}\" callback, which takes\n",
- " // the figure and JSON message as its only arguments.\n",
- " try {\n",
- " var callback = fig[\"handle_\" + msg_type];\n",
- " } catch (e) {\n",
- " console.log(\"No handler for the '\" + msg_type + \"' message type: \", msg);\n",
- " return;\n",
- " }\n",
- "\n",
- " if (callback) {\n",
- " try {\n",
- " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
- " callback(fig, msg);\n",
- " } catch (e) {\n",
- " console.log(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\n",
- " }\n",
- " }\n",
- " };\n",
- "}\n",
- "\n",
- "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n",
- "mpl.findpos = function(e) {\n",
- " //this section is from http://www.quirksmode.org/js/events_properties.html\n",
- " var targ;\n",
- " if (!e)\n",
- " e = window.event;\n",
- " if (e.target)\n",
- " targ = e.target;\n",
- " else if (e.srcElement)\n",
- " targ = e.srcElement;\n",
- " if (targ.nodeType == 3) // defeat Safari bug\n",
- " targ = targ.parentNode;\n",
- "\n",
- " // jQuery normalizes the pageX and pageY\n",
- " // pageX,Y are the mouse positions relative to the document\n",
- " // offset() returns the position of the element relative to the document\n",
- " var x = e.pageX - $(targ).offset().left;\n",
- " var y = e.pageY - $(targ).offset().top;\n",
- "\n",
- " return {\"x\": x, \"y\": y};\n",
- "};\n",
- "\n",
- "/*\n",
- " * return a copy of an object with only non-object keys\n",
- " * we need this to avoid circular references\n",
- " * http://stackoverflow.com/a/24161582/3208463\n",
- " */\n",
- "function simpleKeys (original) {\n",
- " return Object.keys(original).reduce(function (obj, key) {\n",
- " if (typeof original[key] !== 'object')\n",
- " obj[key] = original[key]\n",
- " return obj;\n",
- " }, {});\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.mouse_event = function(event, name) {\n",
- " var canvas_pos = mpl.findpos(event)\n",
- "\n",
- " if (name === 'button_press')\n",
- " {\n",
- " this.canvas.focus();\n",
- " this.canvas_div.focus();\n",
- " }\n",
- "\n",
- " var x = canvas_pos.x * mpl.ratio;\n",
- " var y = canvas_pos.y * mpl.ratio;\n",
- "\n",
- " this.send_message(name, {x: x, y: y, button: event.button,\n",
- " step: event.step,\n",
- " guiEvent: simpleKeys(event)});\n",
- "\n",
- " /* This prevents the web browser from automatically changing to\n",
- " * the text insertion cursor when the button is pressed. We want\n",
- " * to control all of the cursor setting manually through the\n",
- " * 'cursor' event from matplotlib */\n",
- " event.preventDefault();\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " // Handle any extra behaviour associated with a key event\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.key_event = function(event, name) {\n",
- "\n",
- " // Prevent repeat events\n",
- " if (name == 'key_press')\n",
- " {\n",
- " if (event.which === this._key)\n",
- " return;\n",
- " else\n",
- " this._key = event.which;\n",
- " }\n",
- " if (name == 'key_release')\n",
- " this._key = null;\n",
- "\n",
- " var value = '';\n",
- " if (event.ctrlKey && event.which != 17)\n",
- " value += \"ctrl+\";\n",
- " if (event.altKey && event.which != 18)\n",
- " value += \"alt+\";\n",
- " if (event.shiftKey && event.which != 16)\n",
- " value += \"shift+\";\n",
- "\n",
- " value += 'k';\n",
- " value += event.which.toString();\n",
- "\n",
- " this._key_event_extra(event, name);\n",
- "\n",
- " this.send_message(name, {key: value,\n",
- " guiEvent: simpleKeys(event)});\n",
- " return false;\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onclick = function(name) {\n",
- " if (name == 'download') {\n",
- " this.handle_save(this, null);\n",
- " } else {\n",
- " this.send_message(\"toolbar_button\", {name: name});\n",
- " }\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {\n",
- " this.message.textContent = tooltip;\n",
- "};\n",
- "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n",
- "\n",
- "mpl.extensions = [\"eps\", \"jpeg\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\"];\n",
- "\n",
- "mpl.default_extension = \"png\";var comm_websocket_adapter = function(comm) {\n",
- " // Create a \"websocket\"-like object which calls the given IPython comm\n",
- " // object with the appropriate methods. Currently this is a non binary\n",
- " // socket, so there is still some room for performance tuning.\n",
- " var ws = {};\n",
- "\n",
- " ws.close = function() {\n",
- " comm.close()\n",
- " };\n",
- " ws.send = function(m) {\n",
- " //console.log('sending', m);\n",
- " comm.send(m);\n",
- " };\n",
- " // Register the callback with on_msg.\n",
- " comm.on_msg(function(msg) {\n",
- " //console.log('receiving', msg['content']['data'], msg);\n",
- " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
- " ws.onmessage(msg['content']['data'])\n",
- " });\n",
- " return ws;\n",
- "}\n",
- "\n",
- "mpl.mpl_figure_comm = function(comm, msg) {\n",
- " // This is the function which gets called when the mpl process\n",
- " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
- "\n",
- " var id = msg.content.data.id;\n",
- " // Get hold of the div created by the display call when the Comm\n",
- " // socket was opened in Python.\n",
- " var element = $(\"#\" + id);\n",
- " var ws_proxy = comm_websocket_adapter(comm)\n",
- "\n",
- " function ondownload(figure, format) {\n",
- " window.open(figure.imageObj.src);\n",
- " }\n",
- "\n",
- " var fig = new mpl.figure(id, ws_proxy,\n",
- " ondownload,\n",
- " element.get(0));\n",
- "\n",
- " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
- " // web socket which is closed, not our websocket->open comm proxy.\n",
- " ws_proxy.onopen();\n",
- "\n",
- " fig.parent_element = element.get(0);\n",
- " fig.cell_info = mpl.find_output_cell(\"\");\n",
- " if (!fig.cell_info) {\n",
- " console.error(\"Failed to find cell for figure\", id, fig);\n",
- " return;\n",
- " }\n",
- "\n",
- " var output_index = fig.cell_info[2]\n",
- " var cell = fig.cell_info[0];\n",
- "\n",
- "};\n",
- "\n",
- "mpl.figure.prototype.handle_close = function(fig, msg) {\n",
- " var width = fig.canvas.width/mpl.ratio\n",
- " fig.root.unbind('remove')\n",
- "\n",
- " // Update the output cell to use the data from the current canvas.\n",
- " fig.push_to_output();\n",
- " var dataURL = fig.canvas.toDataURL();\n",
- " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
- " // the notebook keyboard shortcuts fail.\n",
- " IPython.keyboard_manager.enable()\n",
- " $(fig.parent_element).html('
');\n",
- " fig.close_ws(fig, msg);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.close_ws = function(fig, msg){\n",
- " fig.send_message('closing', msg);\n",
- " // fig.ws.close()\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.push_to_output = function(remove_interactive) {\n",
- " // Turn the data on the canvas into data in the output cell.\n",
- " var width = this.canvas.width/mpl.ratio\n",
- " var dataURL = this.canvas.toDataURL();\n",
- " this.cell_info[1]['text/html'] = '
';\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.updated_canvas_event = function() {\n",
- " // Tell IPython that the notebook contents must change.\n",
- " IPython.notebook.set_dirty(true);\n",
- " this.send_message(\"ack\", {});\n",
- " var fig = this;\n",
- " // Wait a second, then push the new image to the DOM so\n",
- " // that it is saved nicely (might be nice to debounce this).\n",
- " setTimeout(function () { fig.push_to_output() }, 1000);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._init_toolbar = function() {\n",
- " var fig = this;\n",
- "\n",
- " var nav_element = $('')\n",
- " nav_element.attr('style', 'width: 100%');\n",
- " this.root.append(nav_element);\n",
- "\n",
- " // Define a callback function for later on.\n",
- " function toolbar_event(event) {\n",
- " return fig.toolbar_button_onclick(event['data']);\n",
- " }\n",
- " function toolbar_mouse_event(event) {\n",
- " return fig.toolbar_button_onmouseover(event['data']);\n",
- " }\n",
- "\n",
- " for(var toolbar_ind in mpl.toolbar_items){\n",
- " var name = mpl.toolbar_items[toolbar_ind][0];\n",
- " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
- " var image = mpl.toolbar_items[toolbar_ind][2];\n",
- " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
- "\n",
- " if (!name) { continue; };\n",
- "\n",
- " var button = $('');\n",
- " button.click(method_name, toolbar_event);\n",
- " button.mouseover(tooltip, toolbar_mouse_event);\n",
- " nav_element.append(button);\n",
- " }\n",
- "\n",
- " // Add the status bar.\n",
- " var status_bar = $('');\n",
- " nav_element.append(status_bar);\n",
- " this.message = status_bar[0];\n",
- "\n",
- " // Add the close button to the window.\n",
- " var buttongrp = $('');\n",
- " var button = $('');\n",
- " button.click(function (evt) { fig.handle_close(fig, {}); } );\n",
- " button.mouseover('Stop Interaction', toolbar_mouse_event);\n",
- " buttongrp.append(button);\n",
- " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n",
- " titlebar.prepend(buttongrp);\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._root_extra_style = function(el){\n",
- " var fig = this\n",
- " el.on(\"remove\", function(){\n",
- "\tfig.close_ws(fig, {});\n",
- " });\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._canvas_extra_style = function(el){\n",
- " // this is important to make the div 'focusable\n",
- " el.attr('tabindex', 0)\n",
- " // reach out to IPython and tell the keyboard manager to turn it's self\n",
- " // off when our div gets focus\n",
- "\n",
- " // location in version 3\n",
- " if (IPython.notebook.keyboard_manager) {\n",
- " IPython.notebook.keyboard_manager.register_events(el);\n",
- " }\n",
- " else {\n",
- " // location in version 2\n",
- " IPython.keyboard_manager.register_events(el);\n",
- " }\n",
- "\n",
- "}\n",
- "\n",
- "mpl.figure.prototype._key_event_extra = function(event, name) {\n",
- " var manager = IPython.notebook.keyboard_manager;\n",
- " if (!manager)\n",
- " manager = IPython.keyboard_manager;\n",
- "\n",
- " // Check for shift+enter\n",
- " if (event.shiftKey && event.which == 13) {\n",
- " this.canvas_div.blur();\n",
- " event.shiftKey = false;\n",
- " // Send a \"J\" for go to next cell\n",
- " event.which = 74;\n",
- " event.keyCode = 74;\n",
- " manager.command_mode();\n",
- " manager.handle_keydown(event);\n",
- " }\n",
- "}\n",
- "\n",
- "mpl.figure.prototype.handle_save = function(fig, msg) {\n",
- " fig.ondownload(fig, null);\n",
- "}\n",
- "\n",
- "\n",
- "mpl.find_output_cell = function(html_output) {\n",
- " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
- " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
- " // IPython event is triggered only after the cells have been serialised, which for\n",
- " // our purposes (turning an active figure into a static one), is too late.\n",
- " var cells = IPython.notebook.get_cells();\n",
- " var ncells = cells.length;\n",
- " for (var i=0; i= 3 moved mimebundle to data attribute of output\n",
- " data = data.data;\n",
- " }\n",
- " if (data['text/html'] == html_output) {\n",
- " return [cell, data, j];\n",
- " }\n",
- " }\n",
- " }\n",
- " }\n",
- "}\n",
- "\n",
- "// Register the function which deals with the matplotlib target/channel.\n",
- "// The kernel may be null if the page has been refreshed.\n",
- "if (IPython.notebook.kernel != null) {\n",
- " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n",
- "}\n"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/html": [
- "
"
- ],
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "%matplotlib notebook\n",
- "\n",
- "import json\n",
- "from qupulse.pulses.plotting import plot\n",
- "\n",
- "with open('parameters/free_induction_decay.json', 'r') as parameter_file:\n",
- " example_values = json.load(parameter_file)\n",
- "\n",
- "_ = plot(experiment, {**example_values, 't_trig': 100, 'N_fid_steps': 2})"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.7.2"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/doc/source/examples/14ArithmeticWithPulseTemplates.ipynb b/doc/source/examples/14ArithmeticWithPulseTemplates.ipynb
deleted file mode 100644
index e56aed203..000000000
--- a/doc/source/examples/14ArithmeticWithPulseTemplates.ipynb
+++ /dev/null
@@ -1,305 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {
- "collapsed": true,
- "pycharm": {
- "name": "#%% md\n"
- }
- },
- "source": [
- "# Arithmetic with Pulse Templates\n",
- "\n",
- "Pulse templates support some simple arithmetic operations that make it nearly a vector space. It is implemented via\n",
- "the two classes `ArithmeticPulseTemplate` and `ArithmeticAtomicPulseTemplate`. This notebook demonstrates the direct\n",
- "and indirect (via invoking operators) usage of these two pulse template classes. We start by defining some building\n",
- "blocks."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "outputs": [
- {
- "data": {
- "text/plain": "",
- "image/png": "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\n"
- },
- "metadata": {
- "needs_background": "light"
- },
- "output_type": "display_data"
- }
- ],
- "source": [
- "import math\n",
- "from qupulse.pulses import TablePT, FunctionPT, RepetitionPT, AtomicMultiChannelPT, plotting\n",
- "\n",
- "# define some building blocks\n",
- "sin_pt = FunctionPT('sin(omega*t)', 't_duration', channel='X')\n",
- "cos_pt = FunctionPT('sin(omega*t)', 't_duration', channel='Y')\n",
- "exp_pt = AtomicMultiChannelPT(sin_pt, cos_pt)\n",
- "tpt = TablePT({'X': [(0, 0), (3., 4.), ('t_duration', 2., 'linear')],\n",
- " 'Y': [(0, 1.), ('t_y', 5.), ('t_duration', 0., 'linear')]})\n",
- "\n",
- "complex_pt = RepetitionPT(tpt, 5) @ exp_pt\n",
- "\n",
- "parameters = dict(t_duration=10, omega=math.pi*2/10, t_y=3.4)\n",
- "_ = plotting.plot(complex_pt, parameters, show=False)"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%%\n",
- "is_executing": false
- }
- }
- },
- {
- "cell_type": "markdown",
- "source": [
- "### Operations with pulse templates and scalars\n",
- "Operations between a pulse template and a scalar are implemented via `ArithmeticAtomicPulseTemplate`.\n",
- "\n",
- "#### Scale\n",
- "Given an arbitrary pulse template $P$ and a scalar $\\alpha$ we can scale the amplitude of all channels by multiplying\n",
- "$P$ with $\\alpha$. Multiplying with $\\alpha^{-1}$ (some people call it dividing by $\\alpha$) is also implemented. "
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%% md\n"
- }
- }
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "outputs": [
- {
- "data": {
- "text/plain": "",
- "image/png": "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\n"
- },
- "metadata": {
- "needs_background": "light"
- },
- "output_type": "display_data"
- }
- ],
- "source": [
- "from qupulse.expressions import Expression\n",
- "\n",
- "scaled = 'alpha' * complex_pt\n",
- "\n",
- "# casts alpha implicitly to ExpressionScalar\n",
- "multiplied = complex_pt * 'x + y'\n",
- "divided = complex_pt / 4.4\n",
- "\n",
- "_ = plotting.plot(scaled, {**parameters, 'alpha': 2}, show=False)"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%%\n",
- "is_executing": false
- }
- }
- },
- {
- "cell_type": "markdown",
- "source": [
- "#### Offset\n",
- "You can add and subtract expression like objects to/from arbitrary pulse templates.\n",
- " "
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%% md\n"
- }
- }
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "outputs": [],
- "source": [
- "offset_pt = scaled + 'offset * alpha'\n",
- "\n",
- "diff_pt = 4 - complex_pt"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%%\n",
- "is_executing": false
- }
- }
- },
- {
- "cell_type": "markdown",
- "source": [
- "#### Channel specific operands\n",
- "If you only want to apply an operation to a specific subset of a pulse template's channels you can do this by using a\n",
- "dictionary as the other operand. Channels not in the dictionary are treated as if the dictionary contains the neutral\n",
- "element of the operation i.e. 0 for addition and subtraction and 1 for multiplication and division."
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%% md\n"
- }
- }
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "outputs": [],
- "source": [
- "scaled_x = {'X': 'x_scale'} * complex_pt\n",
- "scaled_x_y = {'X': 'x_scale', 'Y': 2} * complex_pt\n",
- "\n",
- "offset_x = complex_pt + {'X': 2}\n"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%%\n",
- "is_executing": false
- }
- }
- },
- {
- "cell_type": "markdown",
- "source": [
- "### Adding and subtracting pulse templates\n",
- "Addition and subtraction of pulse templates returns an atomic pulse template, the `ArithmeticAtomicPulseTemplate`\n",
- " - Both have the same length\n",
- " - Both are atomic. Otherwise they are interpreted as atomic.\n",
- " - Channels defined in only one of the two operands are implicitly defined as 0 in the other"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%% md\n"
- }
- }
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "outputs": [
- {
- "data": {
- "text/plain": "",
- "image/png": "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\n"
- },
- "metadata": {
- "needs_background": "light"
- },
- "output_type": "display_data"
- },
- {
- "data": {
- "text/plain": "",
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYAAAAEGCAYAAABsLkJ6AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8li6FKAAAcxElEQVR4nO3dfZRU1ZX38e8GGVsyoiIoCCJtAkJDI2IJKq74giMoCCaDGo0mMJMQH5Mx+jiOCisRY8gYNU/Ik3Ec8AXjivElqGGCGo3GGcRk0MZRETDiC5gGVIQJRAUF3fNHVcOl6aquqq5bt+69v89arO566Xt3AX32Ofuce665OyIikj6dog5ARESioQQgIpJSSgAiIimlBCAiklJKACIiKbVX1AGUokePHt6/f/+owxARiZWlS5e+5+49Wz8fqwTQv39/mpqaog5DRCRWzGxNW8+rBCQiklJKACIiKaUEICKSUrGaAxCR5Ni+fTvNzc1s27Yt6lASo66ujr59+9KlS5ei3q8EICKRaG5uZt9996V///6YWdThxJ67s3HjRpqbm6mvry/qZ1QCEpFIbNu2jQMPPFCNf4WYGQceeGBJIyolABGJjBr/yir171MJQEQkpZQAREQCpkyZwvz58yM59+rVqxk6dOhuz23bto1BgwaxbNmync/dcMMNXHTRRR0+nyaBRURqWF1dHbNnz+biiy9m0aJFrFu3jjlz5lRkVwSNAEQkte666y6GDRvGkUceyYUXXrjz+UWLFnH88cdz+OGH7xwNvP/++4wZM4YRI0bQ2NjIggULgGyvffDgwXz9619nyJAhnHbaaWzduhWAk046iSuvvJKRI0cycOBAnn76aQA++eQTrrjiCo455hiGDRvGnDlzCsY5btw4evfuzV133cVll13GzJkzOeCAAzr8+TUCEJHIXfvr5axYt6Wix2w4pBvXnDkk7+vLly9n1qxZPPPMM/To0YNNmzbtfG39+vUsXryYV155hYkTJzJ58mTq6up46KGH6NatG++99x7HHnssEydOBGDVqlXcc8893HrrrZxzzjk88MADXHDBBQDs2LGDZ599lkceeYRrr72WJ554gttvv5399tuP5557jo8++ojRo0dz2mmnFZzEnT17NiNHjmTAgAG7JauOUAIQkVT63e9+x+TJk+nRowcA3bt3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\n"
- },
- "metadata": {
- "needs_background": "light"
- },
- "output_type": "display_data"
- }
- ],
- "source": [
- "_ = plotting.plot(exp_pt + tpt, parameters, show=False)\n",
- "_ = plotting.plot(exp_pt - tpt, parameters, show=False)\n",
- "\n",
- "combined = exp_pt + tpt"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%%\n",
- "is_executing": false
- }
- }
- },
- {
- "cell_type": "markdown",
- "source": [
- "### Manual creation\n",
- "For exact control what is needed we can use the classes directly instead of implicitly via the operators"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%% md\n"
- }
- }
- },
- {
- "cell_type": "code",
- "execution_count": 6,
- "outputs": [
- {
- "name": "stderr",
- "text": [
- "C:\\Users\\humpohl\\Documents\\git\\qupulse\\qupulse\\pulses\\arithmetic_pulse_template.py:60: ImplicitAtomicityInArithmeticPT: ArithmeticAtomicPulseTemplate treats all operands as if they are atomic. You can silence this warning by passing `silent_atomic=True` or by ignoring this category.\n",
- " category=ImplicitAtomicityInArithmeticPT)\n"
- ],
- "output_type": "stream"
- }
- ],
- "source": [
- "from qupulse.pulses import ArithmeticPT, ArithmeticAtomicPT\n",
- "\n",
- "scaled_x = ArithmeticPT({'X': 'x_scale'}, '*', complex_pt, identifier='scaled_x')\n",
- "\n",
- "# this raises a warning because complex_pt is treated as atomic\n",
- "complex_added_1 = ArithmeticAtomicPT(complex_pt, '+', complex_pt)\n",
- "\n",
- "# this raises a warning because complex_pt is treated as atomic\n",
- "complex_added_2 = ArithmeticAtomicPT(complex_pt, '+', complex_pt, silent_atomic=True)"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%%\n",
- "is_executing": false
- }
- }
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 2
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython2",
- "version": "2.7.6"
- },
- "pycharm": {
- "stem_cell": {
- "cell_type": "raw",
- "source": [],
- "metadata": {
- "collapsed": false
- }
- }
- }
- },
- "nbformat": 4,
- "nbformat_minor": 0
-}
\ No newline at end of file
diff --git a/doc/source/examples/16TimeReversal.ipynb b/doc/source/examples/16TimeReversal.ipynb
deleted file mode 100644
index 9d3c950ce..000000000
--- a/doc/source/examples/16TimeReversal.ipynb
+++ /dev/null
@@ -1,157 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "source": [
- "# Reversing a Pulse Template\n",
- "\n",
- "The `TimeReversalPulseTemplate` allows to reverse arbitrary pulse templates. Let us start with a pulse that has a clear time ordering."
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%% md\n"
- }
- }
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "outputs": [
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "C:\\Users\\Simon\\Documents\\git\\qupulse\\qupulse\\utils\\sympy.py:26: UserWarning: scipy is not installed. This reduces the set of available functions to those present in numpy + manually vectorized functions in math.\n",
- " warnings.warn('scipy is not installed. This reduces the set of available functions to those present in numpy + '\n"
- ]
- },
- {
- "data": {
- "text/plain": "",
- "image/png": "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\n"
- },
- "metadata": {
- "needs_background": "light"
- },
- "output_type": "display_data"
- }
- ],
- "source": [
- "from qupulse.pulses import TimeReversalPT, FunctionPT, TablePT, plotting\n",
- "\n",
- "forward_1 = FunctionPT('sin(2*pi*t / 10)', duration_expression='10', channel='A')\n",
- "wait = TablePT({'A': [(0, 0), (3, 0)]}, measurements=[('M', 0.5, 1)])\n",
- "forward_2 = FunctionPT('sin(2*pi*t / 5)', duration_expression='5', channel='A')\n",
- "\n",
- "forward_all = forward_1 @ wait @ forward_2\n",
- "\n",
- "_ = plotting.plot(forward_all, plot_measurements={'M'}, show=False)"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%%\n"
- }
- }
- },
- {
- "cell_type": "markdown",
- "source": [
- "We can now easily create the same pulse backward"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%% md\n"
- }
- }
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "outputs": [
- {
- "data": {
- "text/plain": "",
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYoAAAEGCAYAAAB7DNKzAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAgpUlEQVR4nO3de5hcdZ3n8fc3MRjMkLABo02HNo0b1IAYQ0OTUTBeGEhUWlhmhOCjrPoEnLCPjvisEfZR2PEyouMNXSQs7ICTC3iJiRhuSmLWFZokTAi5mAukiZ1EEhNJDCSSwHf/qFNNpVJ1urq6zrU+r+fpp6vOOVX1zcnv9Ld+12PujoiISDVDkg5ARETSTYlCRERCKVGIiEgoJQoREQmlRCEiIqFelXQAUTjxxBN93LhxSYchMXj++Rd46aWXB/0+Q4cOYcSI1zQgIpFsWrly5Z/c/bWV9uUyUYwbN44VK1YkHYbE4IH7lzFmzKmDfp+dOzdywYXnNSAikWwys2eq7VPTk4iIhFKiEBGRUEoUIiISSolCRERCKVGIiEgoJQoREQmlRCEiIqGUKEREJJQShYiIhFKiEBGRUEoUIiISSolCRERCKVGIiEioRBOFmd1hZjvNbE2V/WZm3zOzzWa22swmxR2jiEizS7pG8W/AhSH7pwLjg58ZwC0xxCQiIiUSTRTuvgzYE3JIF3CXFzwKHG9mLfFEJyJS2dzurXz41keY27016VBikfYbF7UCfyh53hts21F+oJnNoFDroK2tLZbgsmxu91YWrtrW97xrYivTO3XeRCopv166t+zp+90M11HaE4VV2OaVDnT32cBsgI6OjorHyCsFvljQO9tH071lT1+Bz2tBF6lHpeul+PuFF1/iNccM7Ts2z9dR2hNFL3ByyfOxwPaEYsmFhau2sW7HPjrbR/cV5uLFsG7HPoBcFXCRelRKEP398S99TfF1ebmW0p4oFgHXmNl8oBPY6+5HNTvJwExoGcndV03uez69s43pnW18+NZHEoxKJD0qfaHqT/E6mtu9lesWPMnCVdtykyiSHh47D3gEeJOZ9ZrZJ8zsajO7OjhkMfA0sBm4DfjHhELNvGLnW7HWUM26HfuaqpNOpNzc7q10b9nT94VqoH/sp3e20dk+OlfXUqI1Cne/vJ/9DsyMKZxcK35DmtAykq6JrRWPKW5XE5Q0s2LndLXrpBZ5u5aSnkchMervG9L0zjbuvmoyE1pGxhyZSPJKa92d7aMH9ce99FrKQ80i7X0UIiKxqKXWPVB5qVmoRpFztfZNlOvesifT34BEalV6jdTbL1FNXmoWShQ5V8+3pOJxpROJRPIqippEua6JrX3JIovXlZqemkD5cNj+TO9sy2RhFqnXQK+Rgcr6EHTVKESkKdXbLDtYWWzWVaIQkaYUR5NTuaw266rpSUSaVtRNTuWy2qyrGkWOFWeY1ivLozREwgz22hisrF1bqlHk2GBmmOZl/LdIJY2YfV2vLF5bqlHkXL0zTDVLW/JusLOv65XFa0uJQkSaRlIjnarJShOUmp5EpGkkMdKpmiw1QSlRiEhTiXukUzVZmoSnpqccanT1OivVY5Fq0tbkVC7t15hqFDnUyOp1lqrHItWkqcmpXBauMSWKnGpU9TpL1WORMGlpciqXhWtMTU8iIhJKiUJEcivtfRPl0rpgoBKFiORWmvsmyqV5wUD1UYhIrqW1b6JcmhcMVI1CRERCKVHkSNTtsWkf6y1SKukVYuuVxutMTU85EmV7bBbGeouUSnKF2Hql9TpTjSJniu2xjS5gWVzxUiSpFWLrldbrTIlCRERCKVGISK5kbe5ENWnqq1AfhYjkSpbmTlSTtr4KJQoRyZ2szJ2oJm3rP6npSUREQilRiIhIKCWKHIi78y6tC5dJc8tLJ3a5NHRqK1HkQJydd2leuEyaWx46sct1TWxlQstI1u3Yl+g1p87snIir8y7NC5eJZL0Tu1xaOrVVoxARkVBKFCIiEkqJQkQyLa+d2OWSHESSaKIwswvNbIOZbTazWRX2TzGzvWa2Kvj5YhJxikh65bETu1zSg0gS68w2s6HAD4DzgV5guZktcvd1ZYf+X3f/QOwBikhm5K0Tu1zSg0iSrFGcDWx296fd/UVgPtCVYDwiIlJBkomiFfhDyfPeYFu5yWb2hJndZ2anVXszM5thZivMbMWuXbsaHWsqJdk2m4ZJQCJZvYtdvZK67pKcR2EVtnnZ88eBN7j7fjObBvwcGF/pzdx9NjAboKOjo/x9cimpttm0rWwpzSuLd7GrV5LXXZI1il7g5JLnY4HtpQe4+z533x88XgwMM7MT4wsx/aK6o12YtN6FS5pT1u5iV68kr7skE8VyYLyZtZvZMcBlwKLSA8zs9WZmweOzKcS7O/ZIRUSaWGJNT+5+2MyuAR4AhgJ3uPtaM7s62P9D4FLgU2Z2GDgAXObuTdGsJCKSFomu9RQ0Jy0u2/bDksffB74fd1wiIvIKzcwWkcxpltnY1cQ9+kmrx4pI5jTDbOxqkhj9pEQhIpmU99nY1SSx9LiankREJJQShYiIhFKiyKi0LF2gpTwkTs3eiV0urutPfRQZlYalC7SUh8StmTuxy8V5/alGkWFJL12gpTwkCUksW5NGcV5//dYozKwDOBc4icLs6DXAr9w9+XYPERGJXNUahZldaWaPA18AjgU2ADuBdwIPmdmdZtbcKV1EpAmE1ShGAO9w9wOVdprZRApLfqsXU0QiNbd76xH9E3KkYqd218TWSJrkqiYKd/9B2AvdfVXDoxERqUCd2NXF0ald16gnM/uAu9/b6GBERKpp1pnY/Yljpna9o57OamgUIiKSWnUlCnf/UqMDERGRdKpleOxHK21397saH46IiKRNLTWKs0p+zgVuAC6KMCYJkdYlDLSUh0QlLcvVZEH3lj2RXIP91ijc/b+VPjezUcCPGh6J1CSNoz+0lIdEKQ3L1WRB18RWurfsYeGqbQ2/BusZ9fQChfkTkpC0jf5IYn18aS5JL1eTBdM72/qSaqPV0kfxC8CDp0OACcA9kUQjIiKpU0uN4psljw8Dz7h7b0TxiIhIytTSR/GbOAIRESmlZTvqE8VyHnXNozCz2Q35dBGRKtI4cCPtuia2MqFlJOt27Gtof0W9Ny66tWERiIhUkbaBG2kX1cCSemdmr2xoFCIiklq1jHp6LfB5CqOdhhe3u/t7IoxLRERSopYaxRxgPdAO3Aj0AMsjjElERFKklkRxgrvfDhxy99+4+8eBcyKOS8qkdemOclEtISDNIytlPe0auaxOLYniUPB7h5m938zeDowd9CfLgGRhBEgxrqhmh0pzyEJZT7tGj36qZdTTl4P1na4FbgZGAv806E+WAUv7CJAolxCQ5pL2sp52jR79VMuEu+Kd7PYC727Ip4qISGZUbXoys/9hZqND9r/HzD4QTVgiIpIWYTWKJ4FfmNlB4HFgF4XhseOBicCvgK9GHaCINAct2RGNRizpUTVRuPtCYKGZjQfeAbQA+4B/B2a4+4G6PlFEpAJ1Yjdeo+4VU0sfxSZgU13vLiIyAOrEbqxGdWrXtYSHiIg0j0QThZldaGYbzGyzmc2qsN/M7HvB/tVmNimJOEVEmlliicLMhgI/AKZSWEfqcjObUHbYVAqd5+OBGcAtsQYpIiI1LQp4KoU/0K9z99PN7AzgInf/8iA/+2xgs7s/HXzOfKALWFdyTBdwl7s78KiZHW9mLe6+o94PvfEXa1m3vdCx08gbe0QliyNBiqMs4vDnPS8w7JgNg36fQy8e4I5n+o85C2Uma7JYxptNLTWK24AvECzl4e6rgcsa8NmtwB9KnvcG2wZ6DABmNsPMVpjZil27dvX74Y2+sUdUsjYSpLh0QB5lpcxkTdbKeBZNOGkkE06q/7qsZQmP17j7Y2ZWuu1w3Z/4Cquwzes4prDRfTYwG6Cjo6PiMQBf+uBpALF9422ELI0EKY6yiMsD9y9jzJhTB/0+O3du5IILw89xlspM1mSpjGdR8e9evWqpUfzJzN5I8AfazC4F6m76KdELnFzyfCywvY5jREQkQrUkipkUbn36ZjPbBnwG+FQDPns5MN7M2s3sGArNWYvKjlkEfDQY/XQOsHcw/RMiIjJwtUy4exp4n5mNAIa4+18a8cHuftjMrgEeAIYCd7j7WjO7Otj/Q2AxMA3YDLwA/NdGfLaIiNSullFPny17DoWVZFe6+6rBfLi7L6aQDEq3/bDksVOo0YikQiPWzZFXzO3eSveWPXS2V11/VFKgls7sjuDnF8Hz91NoNrrazH7s7jdFFZxImjRq3Rx5RXEUmUY7pVtNt0IFJrn7te5+LYWk8VrgPODKCGMTSZXpnW3cfdXk3A7/TUpn+2gl3ZSrJVG0AS+WPD8EvCFYPfavkUQlIiKpUUvT01wKs6IXBs8/CMwLOrfXVX+ZiIjkQb81Cnf/ZwrrLD1HoRP7anf/n+7+vLtfEXF8kSp2TM7t3pp0KBUVO/okfdJedtJubvdWPnzrI339PZJutdQocPcVZraVwh3uMLM2d8/0FZKFjkl19KVTFspO2mnZjmzpt0ZhZheZ2SZgC/Cb4Pd9UQcWtax0TKqjL32yUnbSrrhsh8p3+tXSmf3PwDnARndvB94H/L9IoxIRkdSoJVEccvfdwBAzG+LuS4CJ0YYlIiJpUUsfxXNm9jfAMmCOme2kMavHiohIBtRSo+iisM7SPwH3A08BH4gyKJGs6N6yRyOfBkCjnbKplkTxRXd/2d0Pu/ud7v494PNRByaSdsXROrqZUe002imbakkU51fYNrXRgYhkzfTONi1mVweNdsqeqn0UZvYp4B+BU8xsdcmu49CoJxGRphHWmT2XwnyJrwGzSrb/xd01XVhEpEmEJYqhwD4q3A/CzEYrWYiINIewRLGS4D7ZgJXtc+CUSCJKQNpuRjO3e+sRnX6SbmkrP2mkMp1tVRNFMAs799K4bo9GhmRHGstPGqlMZ1tNiwKa2UUUblQEsNTd740upHhN72xjemcbH771kaRDOUJxZIikW1rLTxqpTGdXLYsC/gvwaQr3nlgHfNrMvhZ1YCIikg611CimARPd/WUAM7sT+A/gC1EGJiIi6VDLhDuA40sej4ogDpHM082MKtMNuLKvlhrF14D/MLMlFEY/nYdqEyJHUKd2dboBV/ZVrVGY2ffN7G/dfR6F+1H8LPiZ7O7z4wpQJAt0M6NwugFXtoXVKDYB/2pmLcDdwDx3XxVLVCIikhpVaxTu/l13nwy8C9gD/B8zW29mXzSzU2OLUEREEtVvZ7a7P+PuX3f3twPTgYuB9ZFHloCk7y2gtfrzQZ3aBSrP+VHLPIphZvZBM5tDYZHAjcB/iTyymKXh3gKavZp9XRNbmdAyknU79jX9fSpUnvMjbJnx84HLgfcDjwHzgRnu/nxMscVqemdbKi5szV7NNs3UPpLKcz6EdWZfR2Gp8c9ppVgRkeYVtijgu+MMRERE0qnWmdkiItKklChEIpL0KLqkaLRT/ihRiEQgDaPokqLRTvlT0/0oRGRg0jKKLika7ZQvqlGIiEioRGoUZjaawvpR44Ae4B/c/c8VjusB/gK8BBx29474ohQREUiuRjEL+LW7jwd+HTyv5t3uPjGuJJHE8gvq/MuvZlrOQ+U4v5Lqo+gCpgSP7wSWAp9PKJY+Sd1TQJ1/+dRs96hQOc6vpBLF69x9B4C77zCzMVWOc+BBM3PgVnefXe0NzWwGMAOgra2+CzLJ5RfU+Zc/zbich8pxPkWWKMzsV8DrK+y6fgBv8w533x4kkofM7PfuvqzSgUESmQ3Q0dHhAw5YREQqiixRuPv7qu0zs2fNrCWoTbQAO6u8x/bg904zWwCcDVRMFCIiEo2kOrMXAR8LHn8MWFh+gJmNMLPjio+BvwPWxBahSAPlvVN7bvdWurdo7dC8SqqP4l+Ae8zsE8BW4O8BzOwk4H+7+zTgdcACMyvGOdfd708oXpG6NUOndnFyoTqx8ymRROHuu4H3Vti+HZgWPH4aeFvMoYk0XLN0ane2j85lEhTNzBYRkX4oUVQRV5uy2nabS976KjTJrjloUcAK4mxTVttu88hjX4Um2TUH1SgqmN7Zxt1XTWZCy8hYPk9tu80h7nIVl+IkO5Xh/FKiEBGRUEoUIiISSolCJAFZv02qOrGbixKFSMzycJtUdWI3F416EolZXm6TqpVim4dqFCIiEkqJoh9RTZBSG69kcfKdym1zUtNTiCgnSKmNt7lldfKdym1zUqIIEfVibmrjbV5ZXihQ5bb5qOlJRERCKVGIJCwrfRVawLJ5qelJJEFZ6qvQApbNSzUKkQRlbaFALWDZnJQoREQklBJFjRq1No/GoUs1ae2rUJkVJYoaNHJtHo1Dl0q6JrYyoWUk63bsS93yHiqzos7sGjR6bR6NQ5dyaZ9XkWSZPXToEL29vRw8eDCRz8+b4cOHM3bsWIYNG1bza5QoRFKm2ATVNbE10Y7jud1bj6hNJKW3t5fjjjuOcePGYWaJxZEH7s7u3bvp7e2lvb295tep6UkkRdLUBJWWJqeDBw9ywgknKEk0gJlxwgknDLh2phqFSIqkrQkqLc2kShKNU8+5VI1CRERCKVEMwGCGL2qIoQxUUrdLVVmtzZVXXslPfvKTRD67p6eH008/ver+b3/72wwfPpy9e/c25POUKGo02LbjtLT3SjYkebtUldXsmzdvHmeddRYLFixoyPupj6JGjWg7Tkt7r6RfcUh23COgigv/dbaPTmVZvfEXa1m3vbE1nQknjeRLHzwt9Ji77rqLb37zm5gZZ5xxBj/60Y8AWLZsGd/61rf44x//yE033cSll17K/v376erq4s9//jOHDh3iy1/+Ml1dXfT09DB16lTe+c538rvf/Y7W1lYWLlzIsccey5QpU+js7GTJkiU899xz3H777Zx77rm89NJLzJo1i6VLl/LXv/6VmTNnctVVV4XG+tRTT7F//36+8Y1v8NWvfpUrr7xy0OdINQqRlEpiBJQW/jva2rVr+cpXvsLDDz/ME088wXe/+92+fTt27OC3v/0t9957L7NmzQIK8xQWLFjA448/zpIlS7j22mtxdwA2bdrEzJkzWbt2Lccffzw//elP+97r8OHDPPbYY3znO9/hxhtvBOD2229n1KhRLF++nOXLl3PbbbexZcuW0HjnzZvH5ZdfzrnnnsuGDRvYuXPnoM+BahQiKZXUCKg0L/zX3zf/KDz88MNceumlnHjiiQCMHj26b9+HPvQhhgwZwoQJE3j22WeBwlyF6667jmXLljFkyBC2bdvWt6+9vZ2JEycCcOaZZ9LT09P3XpdccslR2x988EFWr17d1xeyd+9eNm3axKmnnlo13vnz57NgwQKGDBnCJZdcwo9//GNmzpw5qHOgRFGHgTYHlFbnReoRdRNUWibXpZG7Vx1S+upXv/qI4wDmzJnDrl27WLlyJcOGDWPcuHF98xZKjx86dCgHDhw46r2GDh3K4cOH+97z5ptv5oILLjjic0sTTKnVq1ezadMmzj//fABefPFFTjnllEEnCjU9DVA9zQGqzstgxNEEpQ7s6t773vdyzz33sHv3bgD27Am/edPevXsZM2YMw4YNY8mSJTzzzDN1f/YFF1zALbfcwqFDhwDYuHEjzz//fNXj582bxw033EBPTw89PT1s376dbdu2DSoGUKIYsHrvH5Dm6rykW2mZa/QKs6VDYYuDLVROj3Taaadx/fXX8653vYu3ve1tfPaznw09/oorrmDFihV0dHQwZ84c3vzmN9f92Z/85CeZMGECkyZN4vTTT+eqq67qq21UMn/+fC6++OIjtl188cXMnz+/7hgArFhdypOOjg5fsWJFpJ9RbDeuZWTIQI6VgXng/mWMGVO9vbZWO3du5IILz2tARNEpbx5qRHkqTRJJry1Vzfr163nLW96SdBi5UumcmtlKd++odLxqFIPQ37c7TVySRiqtWQx2Mp5qEjIQShR1qqXdWO2+EoVGTMZT2ZSB0KinOtU6dFGT7KTRBjMZL4rmK8m/RGoUZvb3ZrbWzF42s4ptYsFxF5rZBjPbbGaz4oxxIMqboNTkJFEr1mi7t+zhugVP1tTBPbd7K9cteJLuLXtUk5ABSapGsQa4BLi12gFmNhT4AXA+0AssN7NF7r4unhBrU7zYikmh9NueLkaJSrFGW6whdG/ZQ/eWPSxcte2oGkbpMQBfvfit6o+QAUkkUbj7euh3XfSzgc3u/nRw7HygC0hVoihtgirWLFStl7iEJYyiYoLobB+d2pFNkm5p7qNoBf5Q8rwX6Kx2sJnNAGYAtLXFfyGU1hxUk5C4lSeMUnlLEI8+upK9z1WfdDZQo44fwTnnnBl6jJnxkY98pG8xwMOHD9PS0kJnZyf33ntvw2JJq8gShZn9Cnh9hV3Xu/vCWt6iwraqkz7cfTYwGwrzKGoKsoGKF6pIkpqhHO597vmGzJ0p2rlzY7/HjBgxgjVr1nDgwAGOPfZYHnroIVpbm+fLYGSd2e7+Pnc/vcJPLUkCCjWIk0uejwW2Nz5SEZH+TZ06lV/+8pfAKyu0Nos0z6NYDow3s3YzOwa4DFiUcEwi0qQuu+wy5s+fz8GDB1m9ejWdnVVbwnMnqeGxF5tZLzAZ+KWZPRBsP8nMFgO4+2HgGuABYD1wj7uvTSJeEZEzzjiDnp4e5s2bx7Rp05IOJ1ZJjXpaABx1jz533w5MK3m+GFgcY2giIlVddNFFfO5zn2Pp0qV9q8k2gzSPehIRSZWPf/zjjBo1ire+9a0sXbo06XBio0QhIpky6vgRNY1UGsj71Wrs2LF8+tOfbthnZ4UShYhkSn9zHqKwf//+o7ZNmTKFKVOmxB5LEtI86klERFJAiUJEREIpUYhI6uXxTpxJqedcKlGISKoNHz6c3bt3K1k0gLuze/duhg8fPqDXqTNbRFJt7Nix9Pb2smvXrqRDyYXhw4czduzYAb1GiUJEUm3YsGG0t7cnHUZTU9OTiIiEUqIQEZFQShQiIhLK8jiSwMx2Ac+EHHIi8KeYwmkExRu9rMWseKPVjPG+wd1fW2lHLhNFf8xshbt3JB1HrRRv9LIWs+KNluI9kpqeREQklBKFiIiEatZEMTvpAAZI8UYvazEr3mgp3hJN2UchIiK1a9YahYiI1EiJQkREQuU6UZjZhWa2wcw2m9msCvvNzL4X7F9tZpOSiDOI5WQzW2Jm681srZkddb9FM5tiZnvNbFXw88UkYi2Jp8fMngxiWVFhf5rO75tKztsqM9tnZp8pOybx82tmd5jZTjNbU7JttJk9ZGabgt//qcprQ8t7jPF+w8x+H/yfLzCz46u8NrT8xBjvDWa2reT/fVqV16bl/N5dEmuPma2q8trGnV93z+UPMBR4CjgFOAZ4AphQdsw04D7AgHOA7gTjbQEmBY+PAzZWiHcKcG/S57Yknh7gxJD9qTm/FcrGHylMMErV+QXOAyYBa0q23QTMCh7PAr5e5d8UWt5jjPfvgFcFj79eKd5ayk+M8d4AfK6GMpOK81u2/1+BL0Z9fvNcozgb2OzuT7v7i8B8oKvsmC7gLi94FDjezFriDhTA3Xe4++PB478A64HWJGJpoNSc3zLvBZ5y97DZ+4lw92XAnrLNXcCdweM7gQ9VeGkt5b3hKsXr7g+6++Hg6aPAwNa0jlCV81uL1JzfIjMz4B+AeVHHkedE0Qr8oeR5L0f/4a3lmNiZ2Tjg7UB3hd2TzewJM7vPzE6LN7KjOPCgma00sxkV9qfy/AKXUf3iStP5LXqdu++AwhcKYEyFY9J6rj9OoVZZSX/lJ07XBE1ld1Rp2kvj+T0XeNbdN1XZ37Dzm+dEYRW2lY8FruWYWJnZ3wA/BT7j7vvKdj9OobnkbcDNwM9jDq/cO9x9EjAVmGlm55XtT+P5PQa4CPhxhd1pO78DkcZzfT1wGJhT5ZD+yk9cbgHeCEwEdlBozimXuvMLXE54baJh5zfPiaIXOLnk+Vhgex3HxMbMhlFIEnPc/Wfl+919n7vvDx4vBoaZ2Ykxh1kaz/bg905gAYXqealUnd/AVOBxd3+2fEfazm+JZ4tNdsHvnRWOSdW5NrOPAR8ArvCgwbxcDeUnFu7+rLu/5O4vA7dViSNt5/dVwCXA3dWOaeT5zXOiWA6MN7P24FvkZcCismMWAR8NRuecA+wtVvHjFrQ33g6sd/dvVTnm9cFxmNnZFP7/dscX5RGxjDCz44qPKXRgrik7LDXnt0TVb2FpOr9lFgEfCx5/DFhY4ZhaynsszOxC4PPARe7+QpVjaik/sSjrN7u4ShypOb+B9wG/d/feSjsbfn6j7rVP8ofCqJuNFEYrXB9suxq4OnhswA+C/U8CHQnG+k4KVdnVwKrgZ1pZvNcAaymMuHgU+NsE4z0liOOJIKZUn98gntdQ+MM/qmRbqs4vhSS2AzhE4VvsJ4ATgF8Dm4Lfo4NjTwIWl7z2qPKeULybKbTnF8vxD8vjrVZ+Eor3R0H5XE3hj39Lms9vsP3fiuW25NjIzq+W8BARkVB5bnoSEZEGUKIQEZFQShQiIhJKiUJEREIpUYiISCglCpESZnZCycqcfyxZVXS/mf2viD7zM2b20Tped4yZLQsmX4lERsNjRaowsxuA/e7+zQg/41UUlg6Z5K8spDeQ13+JwmJ11ZbJEBk01ShEamCFe1XcGzy+wczuNLMHgzX/LzGzm4K1/+8PlmLBzM40s98Ei7I9UGXl3PdQWFLkcPCapWb2dTN7zMw2mtm5wfbTgm2rgsXrxgev/zlwReQnQJqaEoVIfd4IvJ/CUtP/Dixx97cCB4D3B8niZuBSdz8TuAP4SoX3eQewsmzbq9z9bOAzwJeCbVcD33X3iUAHhVm6UFiW4awG/ZtEKlLbpkh97nP3Q2b2JIWb2twfbH8SGAe8CTgdeChYPmoohaUYyrVQuPdIqeKCkCuD9wJ4BLjezMYCP/NgaWl3f8nMXjSz47xwHxORhlOiEKnPXwHc/WUzO+SvdPa9TOG6MmCtu0/u530OAMMrvTfwUvBeuPtcM+umUIt5wMw+6e4PB8e9Gjg4qH+NSAg1PYlEYwPwWjObDIUl5KvcCGk98J/7ezMzOwV42t2/R2HhujOC7ScAu9z9UMMiFymjRCESAS/cLvNS4Otm9gSFVVT/tsKh91G4L3J/PgysMbNVwJuBu4Lt7wYWDzZekTAaHiuSMDNbAPx3r35Ly7DX/gz4grtvaHxkIgWqUYgkbxaFTu0BCW6g83MlCYmaahQiIhJKNQoREQmlRCEiIqGUKEREJJQShYiIhFKiEBGRUP8fe/M8SyKADxsAAAAASUVORK5CYII=\n"
- },
- "metadata": {
- "needs_background": "light"
- },
- "output_type": "display_data"
- }
- ],
- "source": [
- "backward_all = TimeReversalPT(forward_all)\n",
- "_ = plotting.plot(backward_all, plot_measurements={'M'}, show=False)"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%%\n"
- }
- }
- },
- {
- "cell_type": "markdown",
- "source": [
- "and use it in a composed template."
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%% md\n"
- }
- }
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "outputs": [
- {
- "data": {
- "text/plain": "",
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYoAAAEGCAYAAAB7DNKzAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAjSklEQVR4nO3de7gU1Znv8e8LbMGAYrhoECSg0UlQkOiWrXGSkHg3KsE4Rhlvx5OgM+Y8GjXj7TyJZhKTzMR4Ysx4GzxeBjYxUYIxxGs0GB9FwSFyU0FBsoEjCBHc3C/v+aOrN7033bW7e3d1VXX/Ps+zH7qrq7te1uqqt2utVavM3RERESmkW9wBiIhIsilRiIhIKCUKEREJpUQhIiKhlChERCRUj7gDiMKAAQN82LBhcYdRtzZu3MTOnbtC1+nevRu9e3+sShHFZ9PGTfiunQVft27d+VgdlENn34l6+T4k2Zw5cz5w94H5XqvJRDFs2DBmz54ddxh166knZ7L//oeFrrN69duccuoXqhRRfF557k+MHPqpgq/PW76EY0/4YhUjikdn34l6+T4kmZm9V+g1NT2JiEgoJQoREQmlRCEiIqGUKEREJJQShYiIhFKiEBGRUEoUIiISSolCRERCKVGIiEgoJQoREQmlRCEiIqGUKEREJJQShYiIhIo1UZjZ/Wa22szmF3jdzOwOM1tiZm+Y2VHVjlFEpN7FfUbxAHBqyOunAYcGfxOBu6oQk4iI5Ig1Ubj7TGBdyCrjgIc84xVgPzMbVJ3owk2ZtZyv3/MyU2YtjzsUEalRSTnOJP3GRYOBv+Y8bwmWreq4oplNJHPWwdChQyMLaMqs5Uyfu4JZSzP5bdbSdUyfu4JxowczoSm67UrxsnUEqF46obJKpqQdZ5KeKCzPMs+3orvfC9wL0NjYmHedrpoyazk3TpsHQNPwfmzatpOP7dWdWUvXtVWodrR45dYRoHoJobJKpkLHmYWrNgDx1E/SE0ULcFDO8yHAyjgCya28W8ePbFdZ2demz12hnSxm2V/Ht44fCaB6CaGySp6w48zX73mZWUvXMWXW8qrXUdyd2Z15HLgoGP10LLDe3fdodqqG3J2qYyVNaBpK0/B+cYQleTQN78eEpqGqlyKorJIl7DgzbvTgdutUU9zDY5uBl4G/M7MWM/ufZna5mV0erDIDeBdYAtwH/HNMoQK7d6pCstle4jFl1vK25pNcqpc9qaySq9BxJpvM46ijuEc9ne/ug9y9wd2HuPskd7/b3e8OXnd3v8LdD3H3ke4+O854w8SZ7SUjt1M2S/WSn8oqneKqo6Q3PSVCoV9fueLM9rJbx19jalIpTGWVLEk+zihRFCHfr6989ItMRMqV5OOMEkUnslm+s/4J0FlFnIr5NSYZKqvkSfpxRomiE8Vm+SydVcSj1HqqZyqr5En6cUaJogjFZPkstfPGp5R6qncqq+RJ8nFGiSJEV07R1fwkIsVIw3FGiSJEuafoan4SkWKl4TijRNGJck7R1fwkIqVI+nFGiaKASowMUfNTcqguMor5XqusqictxxkligK6OjJEzU/JobrYrbPvtcqqutJynFGiCNGVkSFqfkoO1UV7Yd9rlVX1peE4o0SRRyUvSNJpvIjkk6bjjBJFHpW6IEmn8SJSSJqOM0oUBVTigiSdxotImLQcZ5QoOohiHhw1P0Wr2DpTPRRPZRWttB1nlCg6qPQ8OGp+il4xdaZ6KJ7KKnppO84oUeRRyXlw1PxUHZ3VmeqheCqr6kjTcUaJQkREQilR5NA8/SIStTQeZ5QocmiefhGJWhqPM0oUHWiefhGJWtqOM0oUIiISSomiSjQuPRqltvfWcz2orOIXdf9EVHWmRFEFGpcenVLae+u9HlRW8YuyfyLKOlOiqAKNS49Wse29qgeVVRJE1T8RZZ0pUYiISCglikA1xjarzVdEohbFcUaJIhD12Ga1+YpI1KI6zihR5IhybLPafEUkalEdZ5QoREQklBKFiIiEUqKgupN0qUO7csqtt3qsA5VV/NJ8nFGioHqTdKlDu7LKqbd6rQOVVfzSfJxRoghUY5IudWhXXqn1Vs91oLKKX1qPM0oUIiISSolCRERC1X2iiONuU+ogFKkvaT/OxJoozOxUM3vLzJaY2fV5Xh9rZuvNbG7w991Kx1Dtu02pg1Ck/qT9ONOjIp9SBjPrDvwSOAloAV4zs8fdfWGHVV909zOijKWad5ua0DRUSUKkDqX5OBPnGcUYYIm7v+vu24CpwLgY4xERkTziTBSDgb/mPG8JlnV0nJn9xcz+YGaHF/owM5toZrPNbPaaNWuKCiCOdsMs9VN0TVfrrp7KX2UVr1o4zsSZKCzPMu/w/HXgk+5+JPAL4LeFPszd73X3RndvHDhwYFEBVLvdMEv9FF3Xlbqrt/JXWcWrFo4zcSaKFuCgnOdDgJW5K7j7BndvDR7PABrMbEAlg6hmu2GWLmSqjHLrrh7LX2UVr7QfZ+JMFK8Bh5rZcDPbCzgPeDx3BTP7hJlZ8HgMmXjXVj1SEZE6FtuoJ3ffYWbfAp4CugP3u/sCM7s8eP1u4Bzgn8xsB7AZOM/dOzZPiYhIhGJLFNDWnDSjw7K7cx7fCdxZ7bhERGS3ur0yO86RCFkaTSJS22rlOFO3iSKukQhZGk0iUvtq5ThTt4kC4hmJkKXRJCL1oRaOM3WdKEREpHNKFCIiEqouE0USOpiy1KFdukrVXz2UvcoqPrV0nKnLRBF3B1OWOrTLU4n6q5eyV1nFp5aOM3WZKCDeDqYsdWiXr6v1V09lr7KKT60cZzq94M7MGoHPAweSuTp6PvCsuyfjnEpERCJV8IzCzC4xs9eBG4C9gbeA1cDfA8+Y2YNmFm+qFBGRyIWdUfQGjnf3zfleNLPRwKFAqnq4sh1MSTqVznY0xX2KKiKVkcTjTFcUPKNw918WShLB63Pd/blowopOUjqYstRRKFJ7knac6aqyOrPNLNJ7WEctCR1MWeooFKlNSTrOdFW5o56OqWgUIiKSWGUlCnf/XqUDERGRZCpmeOxF+Za7+0OVD0dEpL3t27fT0tLCli1b4g6laFd8dm8AFi1aFHMkGbnx9OrViyFDhtDQ0FD0+4u5cVFuM1Mv4ATgdUCJQqqu0qNJannEWa2UVUtLC/vssw/Dhg0juDNyoq1t3cr2DzfTu2cPDhnYJ+5wANhrTSsbt+5gYN9esLWVlpYWhg8fXvT7O216cvf/lfP3TeCzwF5diFmkbJUcTVLrI85qpay2bNlC//79U5EkAD7cvB2A/fYu/hd71LKxrN+yg/79+5d8dlZOH8UmMtdPiMSiUqNJ6mHEWa2UVVqSRFbvnj3o36dn3GG06d+nJ717ZhqQyinLYvoofgd48LQbMAJ4pOQtiYhIKhVzRvFT4Lbg70fAF9z9+kijEhFJuEsuuYTf/OY3sWx72bJlHHHEEQVfv/322+nVqxfr16+vyPaK6aP4U87fS+7eUpEtxyBJ88N3pPn+RaRSmpubOeaYY5g2bVq75Ru37mBt69aSP6/cK7PvLed9cUvqZfW13qkqknYPPfQQo0aN4sgjj+TCCy9sWz5z5kw+97nPcfDBB7edXWxsbeXCr53BUUcdxciRI5k+fTqQOQv4zGc+wze/+U0OP/xwTj75ZDZvzsySNHbsWK677jrGjBnDYYcdxosvvgjAzp07+c53vsMxxxzDqFGjuOeeezqN9Z133qG1tZUf/OAHNDc3ty3PdmhnO9tLUczw2Hw6jzahknhZ/YSmoUoSIkW45XcLWLhyQ0U/c8SB+/K9Mw8v+PqCBQv44Q9/yEsvvcSAAQNYt253q8SqVav485//zJtvvslZZ53FOeecQ89evfiPB6Yw+uAD+eCDDzj22GM566yzAFi8eDHNzc3cd999nHvuuTz66KNccMEFAOzYsYNXX32VGTNmcMstt/Dss88yadIk+vbty2uvvcbWrVs5/vjjOfnkk0M7pJubmzn//PP5/Oc/z1tvvcXq1avZf//96d+nZ1lJAsq/MntOWVsTEUmZP/7xj5xzzjkMGDAAgH79do/++upXv0q3bt0YMWIE77//PgDuzm0/vIVRo0Zx4oknsmLFirbXhg8fzujRowE4+uijWbZsWdtnnX322Xssf/rpp3nooYcYPXo0TU1NrF27lsWLF4fGO3XqVM477zy6devG2Wefza9//esul0Exo54GAteRGe3UK7vc3b/c5a2LiJQg7Jd/VNy94C/4nj17tlsP4PFHf8W6tR8wZ84cGhoaGDZsWNt1C7nrd+/eva3pKfe17t27s2PHjrbP/MUvfsEpp5zSbru5CSbXG2+8weLFiznppJMA2LZtGwcffDBXXHFFKf/lPRRzRjEZWAQMB24BlgGvdWmrIiIpccIJJ/DII4+wdu1agHZNT/l8tGED/QcMpKGhgeeff5733nuv7G2fcsop3HXXXWzfnmkyevvtt9m4cWPB9Zubm7n55ptZtmwZy5YtY+XKlaxYsaJLMUBxiaK/u08Ctgcjny4Fju3SVmOQ5BFPWRr5FC6qOqzFck/D9z0tDj/8cG666Sa++MUvcuSRR3L11VcXXHdt61ZOPPNrzJv7Oo2NjUyePJlPf/rTZW/7G9/4BiNGjOCoo47iiCOO4LLLLms728hn6tSpjB8/vt2y8ePHM3Xq1LbnG7fuYOPWwp+RTzGd2dnej1Vm9hVgJTCkpK0kQFJHPGWNGz2YWUvXMX3uisR1tidFFHVYq+We9O972lx88cVcfPHF7ZY98MAD7Z63trbyzppWPt6vP8++8GLeK7Pnz5/f9vjaa69te/zCCy+0PR4wYEBb01K3bt249dZbufXWW9t9Tt++fdt9VtbSpUv3WPazn/2s7fF+ezewcesONm3bued/MkQxZxQ/MLO+wDXAtcB/At8uaSsJkcQRT1lxT5GQFpWuw1ou9yR/32tZ0qbvyJU7lUcpOn2Huz8RPFwPfKnkLYiISKoVPKMws/9tZgV/apnZl9N+S1QREelcWNPTPOB3Zvacmf27mf2LmX3XzB42s3nAmcCs6oTZNWnq2KvFjlWRerC2dWvJncRx2bpjV0nHmYKJwt2nu/vxwOXAAqA7sAH4L2CMu3/b3dd0Md6qSEvHnqbyEEmvJN6HIp9sfKUcZ4rpo1gMhF8KmAJp6NjTVB4i6Zbkjuys/n160rNHaZNylDvXk4hILF55ZQ7rPyx80Vmp+u7Xm2OPPTp0HTPjggsu4OGHHwYy8zINGjSIpqYmnnjiidD31oJYE4WZnQr8nEyz1n+6+487vG7B66eTubPeJe7+etUDFZHEWP/hRvbf/7CKfd7q1W93uk7v3r2ZP38+mzdvZu+99+aZZ55h8OBkN2VXUlmTAlaCmXUHfgmcRmYeqfPNbESH1U4jc9vVQ4GJwF1VDVJEJHDaaafx+9//Htg9Q2u9KGZSwMPIHKAPcPcjzGwUcJa7/6CL2x4DLHH3d4PtTAXGAQtz1hkHPOSZ2bZeMbP9zGyQu68qdiO3/G4Bs5auS9VFVdmRT0nvU4HMiLKO/Sp/W7eJhr3eCn3f9m2buf+9l0va1sJVGxgxaN+SY4zLlFnLeXj2JvrMK9zF17p1Mxf2qWxdZ0f5RfGdX7hqA1+/p7R6g86/E4W+D+NGD+azCany8847j+9///ucccYZvPHGG1x66aVt943Ijngq52K2NCjmjOI+4AaCqTzc/Q3gvApsezDw15znLcGyUtcBwMwmmtlsM5u9Zk37wVhNw/slfsRTVtpGPk2fu4KFqyp7f4BCRgzaNzX1CJmyee+jXaHrvPfRrorXdVSj/MaNHlzVRL1w1YZE7QejRo1i2bJlNDc3c/rpp7d7LS0jnrIaundjxIHF12Ux6e9j7v5qh2l2KzFYON+8vV7GOpmF7vcC9wI0Nja2rRPHtMRdkcaRTyMG7cuvLjuu7flTT87stA159eq3OeXU40LXqQWf3KcbD3zl0IKvn/PYG5FsN4pRfhOahpb9mZ19J/J9H8o5c4naWWedxbXXXssLL7zQNptsVhpGPGXt97EGvnfmZ4pev5hE8YGZHUJwgDazc4Cim35CtAAH5TwfQmbCwVLXERGpiksvvZS+ffsycuTIdhP51bpiEsUVZH6pf9rMVgBLgQsqsO3XgEPNbDiwgkxz1oQO6zwOfCvov2gC1pfSPyEitafvfr2LGqlUyucVa8iQIVx55ZUV23ZaFHPB3bvAiWbWG+jm7h9VYsPuvsPMvgU8RWZ47P3uvsDMLg9evxuYQWZo7BIyw2P/RyW2LSLp1dk1D1FobW3dY9nYsWMZO3Zs1WOJQzGjnq7u8BwyM8nOcfe5Xdm4u88gkwxyl92d89jJnNHUnTSMfIpydI0IZPaDjU194g4jVK2PeILiRj01kpnvaXDwNxEYC9xnZv8SXWj1Ky0jn9Iyh5akU/Z7VepNdqotbSOeylHUrVCBo9z9Gne/hkziGAh8AbgkwtjqVppuppOGObQkndK0H6RpxFM5ikkUQ4FtOc+3A590983A1kiiEhGRxCimUW0KmauipwfPzwSag87thYXfJiIitaDTMwp3/1cy/RIfkunEvtzdv+/uG939HyOOr64l+SZGaboZVDEqXdallE+S6zkJtu7YxdrWZDZepOlmRV1RVDe9u882s+VALwAzG+ru+mZHaNzowcxauo7pc1cksg+gljqyoyjrbPkc/4nwDs7jP9HAor9tTWw9x23c6MGw8wM+3Ly9rQ9g7qtz2PLRnsNVy9Vrnz6MHtP5NONXX301t912GwA//elPaW1t5cIrrgVquyMbihseexZwG3AgsJpMn8WbQLrmxkiZNEzlUSsd2VGVddPwfpw4ZFvoOicOaWDeluIv+Ko3E5qGMvPV9mdmWz5qZeTQT1VsG/OWL+l0nZ49e/LYY49xww03MGDAgHav1XpHNhTXmf2vwLHA2+4+HDgReCnSqEREEqRHjx5MnDiR22+/Pe5QYlFMotju7muBbmbWzd2fB0ZHG5aISLJcccUVTJ48mfXr18cdStUV00fxoZn1AWYCk81sNZWZPVZEJDX23XdfLrroIu644w723nvvuMOpqmLOKMaRmWfp28CTwDvAGVEGJbslcURMrY14EinWVVddxaRJk9i4cSObtu2oixFPUFyi+K6773L3He7+oLvfAVwXdWCS3Kk8amnEk0gp+vXrx7nnnsukSZPYsj1zU6paH/EExTU9ncSeieG0PMukwpI88qlWRjxJ+vTap09RI5VK+bxSXHPNNdx5551AfYx4gpBEYWb/BPwzcLCZ5d6Gax806klEYtLZNQ9RyJ1m/IADDmDTpk28s6Zy13IkXdgZxRTgD8CPgOtzln/k7mqgFhGpE2GJojuwgTz3gzCzfkoWIiL1Iawzew4wO/ib0+FvdvShiVRXpUaYlTMqLM5tJ50DrVu2J2q+pzTP8ZS5H1xpCiYKdx/u7gcHf8M7/B3cpUhFEqaSI8xKHRUW57bToHuPvdixaQN/2xQ+HUo1pfVmRe7O2rVr6dWrV0nvK2pSwGC+py8ET19w9ydKjE8k0So9wiw7KuyV55bGtu1a0TTyUKa/NI+B6//G9rXJGGG05qPM2U3DRz1ZHXMsperVqxdDhgwp6T3FTAr4Y+AYYHKw6EozO97dbyg9RBGR0jQ0NPDrtzIH5l9dNjreYAI33/MykJx4olbMGcXpwGh33wVgZg8C/w0oUYiI1IFirswG2C/ncd8I4pAQSZrGoxY7SyUdkrQf1JtiEsWPgP82sweCs4k5wK3RhiVZSZvGoxY7SyX5krYf1JuCicLM7jSzz7l7M5n7UTwW/B3n7lOrFWC9m9A0lKbh/eIOo51a6yyV5EviflBPwvooFgO3mdkg4FdAs7vPrUpUIiKSGGHXUfzc3Y8DvgisA/6vmS0ys++a2WFVi1BERGLVaR+Fu7/n7j9x988CE4DxwKLII5N2ktCRVw8d2V0t566UUZzbTgvtB/HoNFGYWYOZnWlmk8lMEvg28LXII5M2SenIq/WO7EqUc7llFOe200L7QXzCOrNPMrP7gRZgIjADOMTdv+7uv61SfEKyOvJquSO7UuVcThnFue200H4Qn7DO7BvJTDV+rWaKFRGpXwUThbt/qZqBiIhIMhV7ZbaIiNQpJYoUiXPERz2O9JBk0n5QfUoUKRH3iI96HOkhyaP9IB5KFCmRhBEf9TbSQ5JH+0E8lChERCRUUXe4qzQz60dm/qhhwDLgXHf/W571lgEfATuBHe7eWL0oRUQE4jujuB54zt0PBZ4LnhfyJXcfrSSREUdHXr114JVbxpUopzi3nSbaD6orrkQxDngwePwg8NWY4kiVuDry6qkDrytl3NVyinPbaaL9oPriShQHuPsqgODf/Qus58DTZjbHzCaGfaCZTTSz2WY2e82aNRUONxni7Mirlw68rpZxV8opzm2nifaD6ousj8LMngU+keelm0r4mOPdfaWZ7Q88Y2ZvuvvMfCu6+73AvQCNjY1ecsAiIpJXZInC3U8s9JqZvW9mg9x9VXBjpNUFPmNl8O9qM5sGjAHyJgoREYlGXE1PjwMXB48vBqZ3XMHMepvZPtnHwMnA/KpFmGDV7Mir5w48STbtB9UTV6L4MXCSmS0GTgqeY2YHmtmMYJ0DgD+b2V+AV4Hfu/uTsUSbINXuyKvnDjxJLu0H1RXLdRTuvhY4Ic/ylcDpweN3gSOrHFriTWgaWvXRHvXagSfJpf2gunRltoiIhFKiSKlqtM/Wc7tsqeVbybKKc9tpo/2gOpQoUqha7bP12i5bTvlWqqzi3HbaaD+oHiWKFKrmBUf12C5bbvlWoqzi3HbaaD+oHiUKEREJpUQhIiKhlChSLMqOPHXgiWg/yFKiSKmoO/LUgSei/SBLiSKlqtGRV+8deCKg/QCUKEREpBNKFCIFFNsHFEU7dpzbTpuo+upUtrspUYjkUUofUKXbsePcdtpE2VdX72WbS4lCJI9S+4Aq2Y4d57bTJuq+unou21xKFCIiEkqJIuWiaJ9V26ykTaX3A+0D7SlRpFhU7bNqm5U0iWI/0D7QnhJFikXZPqu2WUmLqPYD7QO7KVGIiEgoJYoaUMn2WbXNttdZ2cZZXqqr9qpxE6N6pUSRcpVun1Xb7G7FlG2c5aW62q1aNzGqV0oUKRdF+6zaZjOKLds4y0t1lVHNmxjVIyWKGlGJ0241ZUjaaT+IhhJFDajUabeaMiTNtB9ER4miBlTytFtNGZJW2g+io0QhIiKhlChqSFfaZ9Uumzxh9an6Kkz7QeUpUdSIrrbPql02WTqrT9VXftoPoqFEUSOy7bPl/JrK/opSu2xyFNPervrak/aDaChR1JByf03pV5TUEu0HladEUUO6MupDv6KkVmg/qDwlihpUymm3Ou86V6g8q1F2+batOiuO9oPKUaKoMaWedut0O1xYeUZddoW2rTrrnPaDylKiqDGldOap865znTVjRFl2YdtWnYUrpflJ+0HnlChqULG/pvQrSkT7QTGUKGpQMWcV+hVVmo5lWc02bd1noXzF3k9E+0E4JYoapQu2KidfWVar/HSfhfIl/X4iaaJEUaN0wVblFCrLapSf7rNQvs7OrHU2UbxYEoWZ/YOZLTCzXWbWGLLeqWb2lpktMbPrqxljrdDwysrJlmUc5RfnttMse6Zw47R5e+wHOpsoXlxnFPOBs4GZhVYws+7AL4HTgBHA+WY2ojrh1YZ8O8mUWcu5cdq8dq9L53KbMap9gIlz22k3oWkot44fCey5H+hsong94tiouy8CMLOw1cYAS9z93WDdqcA4YGHkAdaI7A5w47R53DhtHtPnrmj7NXrr+JHaQUowoWko0+euYOGqDUB1m+3i3HYtCNsPlHCLE0uiKNJg4K85z1uApkIrm9lEYCLA0KHaibKyO0n2l2jT8H6MGz1YB5oy5B5Uqn2AiXPbtUD7QddElijM7FngE3leusndpxfzEXmWeaGV3f1e4F6AxsbGguvVowlNQ7VDVECc5ag67DqVYfkiSxTufmIXP6IFOCjn+RBgZRc/U0RESpTk4bGvAYea2XAz2ws4D3g85phEROpOXMNjx5tZC3Ac8HszeypYfqCZzQBw9x3At4CngEXAI+6+II54RUTqWVyjnqYB0/IsXwmcnvN8BjCjiqGJiEgHSW56EhGRBFCiEBGRUEoUIiISSolCRERCKVGIiEgoJQoREQmlRCEiIqGUKEREJJQShYiIhFKiEBGRUEoUIiISSolCRERCmXvt3ePHzNYA7+UsGgB8EFM45VC80VK80VK80Yoq3k+6+8B8L9RkoujIzGa7e2PccRRL8UZL8UZL8UYrjnjV9CQiIqGUKEREJFS9JIp74w6gRIo3Woo3Woo3WlWPty76KEREpHz1ckYhIiJlUqIQEZFQNZ0ozOxUM3vLzJaY2fVxx1MMM1tmZvPMbK6ZzY47no7M7H4zW21m83OW9TOzZ8xscfDvx+OMMVeBeG82sxVBGc81s9PjjDGXmR1kZs+b2SIzW2BmVwbLE1nGIfEmsozNrJeZvWpmfwnivSVYntTyLRRvVcu3ZvsozKw78DZwEtACvAac7+4LYw2sE2a2DGh090ReAGRmXwBagYfc/Yhg2b8B69z9x0FC/ri7XxdnnFkF4r0ZaHX3n8YZWz5mNggY5O6vm9k+wBzgq8AlJLCMQ+I9lwSWsZkZ0NvdW82sAfgzcCVwNsks30LxnkoVy7eWzyjGAEvc/V133wZMBcbFHFPquftMYF2HxeOAB4PHD5I5UCRCgXgTy91XufvrweOPgEXAYBJaxiHxJpJntAZPG4I/J7nlWyjeqqrlRDEY+GvO8xYS/AXO4cDTZjbHzCbGHUyRDnD3VZA5cAD7xxxPMb5lZm8ETVOJaGboyMyGAZ8FZpGCMu4QLyS0jM2su5nNBVYDz7h7osu3QLxQxfKt5URheZaloZ3teHc/CjgNuCJoOpHKugs4BBgNrAJuizWaPMysD/AocJW7b4g7ns7kiTexZezuO919NDAEGGNmR8QcUqgC8Va1fGs5UbQAB+U8HwKsjCmWorn7yuDf1cA0Mk1oSfd+0FadbbNeHXM8odz9/WDn2wXcR8LKOGiLfhSY7O6PBYsTW8b54k16GQO4+4fAC2Ta+xNbvlm58Va7fGs5UbwGHGpmw81sL+A84PGYYwplZr2DDkHMrDdwMjA//F2J8DhwcfD4YmB6jLF0KntACIwnQWUcdF5OAha5+89yXkpkGReKN6llbGYDzWy/4PHewInAmyS3fPPGW+3yrdlRTwDBkLH/A3QH7nf3H8YbUTgzO5jMWQRAD2BK0mI2s2ZgLJmpjt8Hvgf8FngEGAosB/7B3RPRgVwg3rFkTtkdWAZclm2fjpuZ/T3wIjAP2BUsvpFMu3/iyjgk3vNJYBmb2SgyndXdyfxQfsTdv29m/Ulm+RaK92GqWL41nShERKTrarnpSUREKkCJQkREQilRiIhIKCUKEREJpUQhIiKhlChEcphZ/5wZOf9fzgydrWb2HxFt8yozu6iM9+1lZjPNrEcUcYlkaXisSAHVmGU2OMi/Dhzl7jvKeP/3yEx+ObniwYkEdEYhUgQzG2tmTwSPbzazB83sacvcP+RsM/s3y9xH5MlgSgvM7Ggz+1MwweNTHa6mzfoy8Ho2SZjZC2b2k+AeBG+b2eeD5YcHy+YGE8EdGrz/t8A/Rl4AUteUKETKcwjwFTLTU/8X8Ly7jwQ2A18JksUvgHPc/WjgfiDfVfbHk7mHQ64e7j4GuIrMleQAlwM/DyaHayQzlxlkpm44pkL/J5G81LYpUp4/uPt2M5tHZnqFJ4Pl84BhwN8BRwDPZKZDojuZWT47GkTmHg65shMBzgk+C+Bl4CYzGwI85u6LITOzqJltM7N9gvtBiFScEoVIebYCuPsuM9vuuzv7dpHZrwxY4O7HdfI5m4Fe+T4b2Bl8Fu4+xcxmkTmLecrMvuHufwzW6wls6dL/RiSEmp5EovEWMNDMjoPMVNxmdnie9RYBn+rsw4IJI9919zvIzHQ6KljeH1jj7tsrFrlIB0oUIhEIbr97DvATM/sLMBf4XJ5V/wAUc3OqrwPzgzudfRp4KFj+JWBGV+MVCaPhsSIxM7NpwL9k+x1KfO9jwA3u/lblIxPJ0BmFSPyuJ9OpXZLghly/VZKQqOmMQkREQumMQkREQilRiIhIKCUKEREJpUQhIiKhlChERCTU/wchO3oek3AQ7gAAAABJRU5ErkJggg==\n"
- },
- "metadata": {
- "needs_background": "light"
- },
- "output_type": "display_data"
- }
- ],
- "source": [
- "intermediate = TablePT({'A': [(0, 0), (1, 0)]}, measurements=[('N', 0, 1)])\n",
- "composed = forward_all @ intermediate @ backward_all\n",
- "_ = plotting.plot(composed, plot_measurements={'M', 'N'}, show=False)"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%%\n"
- }
- }
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 2
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython2",
- "version": "2.7.6"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 0
-}
\ No newline at end of file
diff --git a/doc/source/examples/examples.rst b/doc/source/examples/examples.rst
index 95ebff450..90e3355bd 100644
--- a/doc/source/examples/examples.rst
+++ b/doc/source/examples/examples.rst
@@ -3,28 +3,49 @@
Examples
========
-All examples are provided as static text in this documentation and, additionally, as interactive jupyter notebooks accessible by running `jupyter notebook` in the `/doc/source/examples` directory of the source tree.
+All examples are provided as static text in this documentation and, additionally, as interactive jupyter notebooks accessible by running ``jupyter notebook`` in the ``/doc/source/examples`` directory of the source tree.
+
.. toctree::
+ :caption: Pulse template types
+ :name: pt_types
+
00SimpleTablePulse
- 01AdvancedTablePulse
- 02FunctionPulse
- 03PointPulse
- 03xComposedPulses
- 03ConstantPulseTemplate
- 04PulseStorage
- 05MappingTemplate
- 06CreatePrograms
- 07MultiChannelTemplates
- 08Measurements
- 09ParameterConstraints
- 10FreeInductionDecayExample
- 11GateConfigurationExample
- 12AbstractPulseTemplate
- 13RetrospectiveConstantChannelAddition
- 14ArithmeticWithPulseTemplates
- 15DynamicNuclearPolarisation
- 16TimeReversal
-
-The `/doc/source/examples` directory also contains some outdated examples for features and functionality that has been changed. These examples start with the number nine and are currently left only for reference purposes.
+ 00AdvancedTablePulse
+ 00FunctionPulse
+ 00PointPulse
+ 00ComposedPulses
+ 00ConstantPulseTemplate
+ 00MultiChannelTemplates
+ 00MappingTemplate
+ 00AbstractPulseTemplate
+ 00ArithmeticWithPulseTemplates
+ 00RetrospectiveConstantChannelAddition
+ 00TimeReversal
+
+.. toctree::
+ :caption: Pulse template features
+ :name: pt_feat
+
+ 01PulseStorage
+ 01Measurements
+ 01ParameterConstraints
+
+.. toctree::
+ :caption: Physically motivated examples
+ :name: physical_examples
+
+ 03SnakeChargeScan
+ 03FreeInductionDecayExample
+ 03GateConfigurationExample
+ 04DynamicNuclearPolarisation
+
+.. toctree::
+ :caption: Pulse playback related examples
+ :name: hardware_examples
+
+ 02CreatePrograms
+ 04ZurichInstrumentsSetup
+
+The ``/doc/source/examples`` directory also contains some outdated examples for features and functionality that has been changed. These examples start with an underscore i.e. ``_*.ipynb`` and are currently left only for reference purposes.
If you are just learning how to get around in qupulse please ignore them.
\ No newline at end of file
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 582bafe8c..cd332a029 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -6,6 +6,12 @@
Welcome to qupulse's documentation!
======================================
+``qupulse`` is a python package to write, manage and playback arbitrarily nested quantum control pulses. This documentation contains concept explanations, jupyter notebook examples and the automatically generated API reference. The API reference does not cover parts of qupulse that are explicitly considered an implementation detail like ``qupulse._program``.
+
+You are encouraged to read the concept explanations and interactively explore the linked examples. To do this you can install qupulse via ``python -m pip install -e git+https://github.com/qutech/qupulse.git#egg=qupulse[default]`` which will clone the qupulse into ``./src/qupulse``. You can find the examples in ``doc/source/examples`` and open them with jupyter, Spyder or another IDE of your choice.
+
+There is a :ref:`learners guide ` available to help with an efficient exploration of qupulse's features.
+
Contents:
.. toctree::
@@ -15,6 +21,7 @@ Contents:
concepts/concepts
examples/examples
_autosummary/qupulse
+ learners_guide
qupulse API Documentation
=========================
diff --git a/doc/source/learners_guide.rst b/doc/source/learners_guide.rst
new file mode 100644
index 000000000..94622762d
--- /dev/null
+++ b/doc/source/learners_guide.rst
@@ -0,0 +1,100 @@
+.. _learners_guide:
+
+Learners Guide - writing pulses with qupulse
+--------------------------------------------
+
+This is a little guide through the documentation of qupulse with the idea that *you* as an interested person can find the materials corresponding to the desired skills.
+
+The following steps assume that you have qupulse installed and are able to run the example notebooks.
+
+
+Basic pulse writing
+^^^^^^^^^^^^^^^^^^^
+
+.. topic:: Info
+
+ **Estimated time:**
+ 30 minutes for reading
+ 60 minutes for the examples
+ 60 minutes for experimenting
+
+ **Target group:**
+
+ **Learning Goals:** The learner is able to define a parameterized nested pulse template.
+
+**Learning Task 1:** Read the concept section about :ref:`pulsetemplates`.
+
+**Exercise Task 1:** Go through the following examples that introduce the shipped atomic pulse templates:
+
+* :ref:`/examples/00SimpleTablePulse.ipynb`
+* :ref:`/examples/00AdvancedTablePulse.ipynb`
+* :ref:`/examples/00FunctionPulse.ipynb`
+* :ref:`/examples/00PointPulse.ipynb`
+* :ref:`/examples/00ConstantPulseTemplate.ipynb`
+
+**Exercise Task 2:** Go through the following examples that introduce the most important composed pulse templates:
+
+* :ref:`/examples/00ComposedPulses.ipynb`
+* :ref:`/examples/00MappingTemplate.ipynb`
+* :ref:`/examples/00MultiChannelTemplates.ipynb`
+
+**Exercise Task 3:** Go through the following examples that introduce other useful pulse templates:
+
+* :ref:`/examples/00ArithmeticWithPulseTemplates.ipynb`
+* :ref:`/examples/00RetrospectiveConstantChannelAddition.ipynb`
+* :ref:`/examples/00TimeReversal.ipynb`
+
+Pulse template features
+^^^^^^^^^^^^^^^^^^^^^^^
+
+.. topic:: Info
+
+ **Estimated time:**
+ 20 minutes for reading
+ 60 minutes for the examples
+ 60 minutes for experimenting
+
+ **Target group:**
+
+ **Learning Goals:** The learner to save pulse templates to the file system.
+ The learner can use pulse identifiers measurement windows and parameter constraints as needed. The learner is able to verify pulse and measurement windows are as intended for a given parameter set by plotting and inspecting. The learner can load pulses from a file and other valid datasources and use them as a building block in their own pulses.
+
+
+**Learning Task 2:** Read the concept section about :ref:`serialization`.
+
+**Exercise Task 4:** Go through the :ref:`/examples/01PulseStorage.ipynb` example. It shows how to load and store pulse templates to disk.
+
+**Exercise Task 5:** Go through the :ref:`/examples/01Measurements.ipynb` example. It shows how to define and inspect measurement windows.
+
+**Exercise Task 6:** Go through the :ref:`/examples/01ParameterConstraints.ipynb` example. It shows how to use parameter constraints to enforce invariants.
+
+**Exercise Task 7:** Go through the :ref:`/examples/03SnakeChargeScan.ipynb` example which shows a realistic pulse.
+
+Hardware capabilities and limitations
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This section introduces aspects of the hardware that are relevant for every experimenter.
+
+This section is incomplete.
+
+.. topic:: Info
+
+ **Estimated time:**
+ 20 minutes for reading
+
+ **Target group:** People who want to use qupulse in an experiment.
+
+ **Learning Goals:**
+ The learner can identify if a hardware limitation related exception that is raised is due to an error on their end and mitigate it.
+ The learner understands capabilities of at least one type of AWGs.
+
+
+**Learning Task 1:**
+
+Read :ref:`program` and :ref:`awgs`.
+
+
+Setup an experiment
+^^^^^^^^^^^^^^^^^^^
+
+This process is not fully documented yet. qupulse gives you tools for very flexible setup configurations. However, there is an example setup with Zurich Instruments devices in :ref:`/examples/04ZurichInstrumentsSetup.ipynb`.
diff --git a/pylint.rc b/pylint.rc
deleted file mode 100644
index 87c13b755..000000000
--- a/pylint.rc
+++ /dev/null
@@ -1,378 +0,0 @@
-[MASTER]
-
-# Specify a configuration file.
-#rcfile=
-
-# Python code to execute, usually for sys.path manipulation such as
-# pygtk.require().
-#init-hook=
-
-# Add files or directories to the blacklist. They should be base names, not
-# paths.
-ignore=CVS
-
-# Pickle collected data for later comparisons.
-persistent=yes
-
-# List of plugins (as comma separated values of python modules names) to load,
-# usually to register additional checkers.
-load-plugins=
-
-# Use multiple processes to speed up Pylint.
-jobs=1
-
-# Allow loading of arbitrary C extensions. Extensions are imported into the
-# active Python interpreter and may run arbitrary code.
-unsafe-load-any-extension=no
-
-# A comma-separated list of package or module names from where C extensions may
-# be loaded. Extensions are loading into the active Python interpreter and may
-# run arbitrary code
-extension-pkg-whitelist=
-
-# Allow optimization of some AST trees. This will activate a peephole AST
-# optimizer, which will apply various small optimizations. For instance, it can
-# be used to obtain the result of joining multiple strings with the addition
-# operator. Joining a lot of strings can lead to a maximum recursion error in
-# Pylint and this flag can prevent that. It has one side effect, the resulting
-# AST will be different than the one from reality.
-optimize-ast=no
-
-
-[MESSAGES CONTROL]
-
-# Only show warnings with the listed confidence levels. Leave empty to show
-# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
-confidence=
-
-# Enable the message, report, category or checker with the given id(s). You can
-# either give multiple identifier separated by comma (,) or put this option
-# multiple time. See also the "--disable" option for examples.
-#enable=
-
-# Disable the message, report, category or checker with the given id(s). You
-# can either give multiple identifiers separated by comma (,) or put this
-# option multiple times (only on the command line, not in the configuration
-# file where it should appear only once).You can also use "--disable=all" to
-# disable everything first and then reenable specific checks. For example, if
-# you want to run only the similarities checker, you can use "--disable=all
-# --enable=similarities". If you want to run only the classes checker, but have
-# no Warning level messages displayed, use"--disable=all --enable=classes
-# --disable=W"
-disable=cmp-method,range-builtin-not-iterating,old-division,old-octal-literal,suppressed-message,delslice-method,intern-builtin,execfile-builtin,backtick,standarderror-builtin,old-raise-syntax,old-ne-operator,dict-iter-method,basestring-builtin,long-builtin,print-statement,map-builtin-not-iterating,unpacking-in-except,nonzero-method,reduce-builtin,import-star-module-level,getslice-method,parameter-unpacking,raw_input-builtin,unicode-builtin,reload-builtin,zip-builtin-not-iterating,dict-view-method,raising-string,indexing-exception,no-absolute-import,xrange-builtin,metaclass-assignment,round-builtin,long-suffix,coerce-builtin,file-builtin,apply-builtin,filter-builtin-not-iterating,hex-method,using-cmp-argument,input-builtin,setslice-method,useless-suppression,oct-method,coerce-method,buffer-builtin,next-method-called,cmp-builtin,unichr-builtin, unsubscriptable-object, too-few-public-methods, trailing-whitespace,bad-continuation
-
-
-[REPORTS]
-
-# Set the output format. Available formats are text, parseable, colorized, msvs
-# (visual studio) and html. You can also give a reporter class, eg
-# mypackage.mymodule.MyReporterClass.
-output-format=text
-
-# Put messages in a separate file for each module / package specified on the
-# command line instead of printing them on stdout. Reports (if any) will be
-# written in a file name "pylint_global.[txt|html]".
-files-output=no
-
-# Tells whether to display a full report or only the messages
-reports=yes
-
-# Python expression which should return a note less than 10 (10 is the highest
-# note). You have access to the variables errors warning, statement which
-# respectively contain the number of errors / warnings messages and the total
-# number of statements analyzed. This is used by the global evaluation report
-# (RP0004).
-evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
-
-# Template used to display messages. This is a python new-style format string
-# used to format the message information. See doc for all details
-#msg-template=
-
-
-[BASIC]
-
-# List of builtins function names that should not be used, separated by a comma
-bad-functions=map,filter
-
-# Good variable names which should always be accepted, separated by a comma
-good-names=i,j,k,ex,Run,_
-
-# Bad variable names which should always be refused, separated by a comma
-bad-names=foo,bar,baz,toto,tutu,tata
-
-# Colon-delimited sets of names that determine each other's naming style when
-# the name regexes allow several styles.
-name-group=
-
-# Include a hint for the correct naming format with invalid-name
-include-naming-hint=no
-
-# Regular expression matching correct class names
-class-rgx=[A-Z_][a-zA-Z0-9]+$
-
-# Naming hint for class names
-class-name-hint=[A-Z_][a-zA-Z0-9]+$
-
-# Regular expression matching correct class attribute names
-class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
-
-# Naming hint for class attribute names
-class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
-
-# Regular expression matching correct method names
-method-rgx=[a-z_][a-z0-9_]{2,30}$
-
-# Naming hint for method names
-method-name-hint=[a-z_][a-z0-9_]{2,30}$
-
-# Regular expression matching correct inline iteration names
-inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
-
-# Naming hint for inline iteration names
-inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
-
-# Regular expression matching correct function names
-function-rgx=[a-z_][a-z0-9_]{2,30}$
-
-# Naming hint for function names
-function-name-hint=[a-z_][a-z0-9_]{2,30}$
-
-# Regular expression matching correct argument names
-argument-rgx=[a-z_][a-z0-9_]{2,30}$
-
-# Naming hint for argument names
-argument-name-hint=[a-z_][a-z0-9_]{2,30}$
-
-# Regular expression matching correct constant names
-const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
-
-# Naming hint for constant names
-const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
-
-# Regular expression matching correct variable names
-variable-rgx=[a-z_][a-z0-9_]{2,30}$
-
-# Naming hint for variable names
-variable-name-hint=[a-z_][a-z0-9_]{2,30}$
-
-# Regular expression matching correct module names
-module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
-
-# Naming hint for module names
-module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
-
-# Regular expression matching correct attribute names
-attr-rgx=[a-z_][a-z0-9_]{2,30}$
-
-# Naming hint for attribute names
-attr-name-hint=[a-z_][a-z0-9_]{2,30}$
-
-# Regular expression which should only match function or class names that do
-# not require a docstring.
-no-docstring-rgx=^_
-
-# Minimum line length for functions/classes that require docstrings, shorter
-# ones are exempt.
-docstring-min-length=-1
-
-
-[ELIF]
-
-# Maximum number of nested blocks for function / method body
-max-nested-blocks=5
-
-
-[FORMAT]
-
-# Maximum number of characters on a single line.
-max-line-length=100
-
-# Regexp for a line that is allowed to be longer than the limit.
-ignore-long-lines=^\s*(# )??$
-
-# Allow the body of an if to be on the same line as the test if there is no
-# else.
-single-line-if-stmt=no
-
-# List of optional constructs for which whitespace checking is disabled. `dict-
-# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
-# `trailing-comma` allows a space between comma and closing bracket: (a, ).
-# `empty-line` allows space-only lines.
-no-space-check=trailing-comma,dict-separator
-
-# Maximum number of lines in a module
-max-module-lines=1000
-
-# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
-# tab).
-indent-string=' '
-
-# Number of spaces of indent required inside a hanging or continued line.
-indent-after-paren=4
-
-# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
-expected-line-ending-format=
-
-
-[LOGGING]
-
-# Logging modules to check that the string format arguments are in logging
-# function parameter format
-logging-modules=logging
-
-
-[MISCELLANEOUS]
-
-# List of note tags to take in consideration, separated by a comma.
-notes=FIXME,XXX,TODO
-
-
-[SIMILARITIES]
-
-# Minimum lines number of a similarity.
-min-similarity-lines=4
-
-# Ignore comments when computing similarities.
-ignore-comments=yes
-
-# Ignore docstrings when computing similarities.
-ignore-docstrings=yes
-
-# Ignore imports when computing similarities.
-ignore-imports=no
-
-
-[SPELLING]
-
-# Spelling dictionary name. Available dictionaries: none. To make it working
-# install python-enchant package.
-spelling-dict=
-
-# List of comma separated words that should not be checked.
-spelling-ignore-words=
-
-# A path to a file that contains private dictionary; one word per line.
-spelling-private-dict-file=
-
-# Tells whether to store unknown words to indicated private dictionary in
-# --spelling-private-dict-file option instead of raising a message.
-spelling-store-unknown-words=no
-
-
-[TYPECHECK]
-
-# Tells whether missing members accessed in mixin class should be ignored. A
-# mixin class is detected if its name ends with "mixin" (case insensitive).
-ignore-mixin-members=yes
-
-# List of module names for which member attributes should not be checked
-# (useful for modules/projects where namespaces are manipulated during runtime
-# and thus existing member attributes cannot be deduced by static analysis. It
-# supports qualified module names, as well as Unix pattern matching.
-ignored-modules=
-
-# List of classes names for which member attributes should not be checked
-# (useful for classes with attributes dynamically set). This supports can work
-# with qualified names.
-ignored-classes=
-
-# List of members which are set dynamically and missed by pylint inference
-# system, and so shouldn't trigger E1101 when accessed. Python regular
-# expressions are accepted.
-generated-members=
-
-
-[VARIABLES]
-
-# Tells whether we should check for unused import in __init__ files.
-init-import=no
-
-# A regular expression matching the name of dummy variables (i.e. expectedly
-# not used).
-dummy-variables-rgx=_$|dummy
-
-# List of additional names supposed to be defined in builtins. Remember that
-# you should avoid to define new builtins when possible.
-additional-builtins=
-
-# List of strings which can identify a callback function by name. A callback
-# name must start or end with one of those strings.
-callbacks=cb_,_cb
-
-
-[CLASSES]
-
-# List of method names used to declare (i.e. assign) instance attributes.
-defining-attr-methods=__init__,__new__,setUp
-
-# List of valid names for the first argument in a class method.
-valid-classmethod-first-arg=cls
-
-# List of valid names for the first argument in a metaclass class method.
-valid-metaclass-classmethod-first-arg=mcs
-
-# List of member names, which should be excluded from the protected access
-# warning.
-exclude-protected=_asdict,_fields,_replace,_source,_make
-
-
-[DESIGN]
-
-# Maximum number of arguments for function / method
-max-args=5
-
-# Argument names that match this expression will be ignored. Default to name
-# with leading underscore
-ignored-argument-names=_.*
-
-# Maximum number of locals for function / method body
-max-locals=15
-
-# Maximum number of return / yield for function / method body
-max-returns=6
-
-# Maximum number of branch for function / method body
-max-branches=12
-
-# Maximum number of statements in function / method body
-max-statements=50
-
-# Maximum number of parents for a class (see R0901).
-max-parents=7
-
-# Maximum number of attributes for a class (see R0902).
-max-attributes=7
-
-# Minimum number of public methods for a class (see R0903).
-min-public-methods=2
-
-# Maximum number of public methods for a class (see R0904).
-max-public-methods=20
-
-# Maximum number of boolean expressions in a if statement
-max-bool-expr=5
-
-
-[IMPORTS]
-
-# Deprecated modules which should not be used, separated by a comma
-deprecated-modules=optparse
-
-# Create a graph of every (i.e. internal and external) dependencies in the
-# given file (report RP0402 must not be disabled)
-import-graph=
-
-# Create a graph of external dependencies in the given file (report RP0402 must
-# not be disabled)
-ext-import-graph=
-
-# Create a graph of internal dependencies in the given file (report RP0402 must
-# not be disabled)
-int-import-graph=
-
-
-[EXCEPTIONS]
-
-# Exceptions that will emit a warning when being caught. Defaults to
-# "Exception"
-overgeneral-exceptions=Exception
diff --git a/pyproject.toml b/pyproject.toml
index 994c5e81e..647bd8b79 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,5 +1,133 @@
[build-system]
-requires = ["setuptools", "wheel"]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "qupulse"
+dynamic = ["version"]
+description = "A Quantum compUting PULse parametrization and SEquencing framework"
+readme = "README.md"
+license = "LGPL-3.0-or-later"
+requires-python = ">=3.10"
+authors = [
+ { name = "Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University" },
+]
+keywords = [
+ "control",
+ "physics",
+ "pulse",
+ "quantum",
+ "qubit",
+]
+classifiers = [
+ "Intended Audience :: Science/Research",
+ "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python :: 3",
+ "Topic :: Scientific/Engineering",
+]
+dependencies = [
+ "frozendict",
+ "lazy_loader",
+ "numpy",
+ "sympy>=1.1.1",
+ "gmpy2",
+]
+
+[project.optional-dependencies]
+autologging = [
+ "autologging",
+]
+default = [
+ "pandas",
+ "scipy",
+ "qupulse[tests,docs,plotting,autologging,faster-sampling]",
+]
+docs = [
+ "ipykernel",
+ "nbsphinx",
+ "pyvisa",
+ "sphinx>=4",
+]
+faster-sampling = [
+ "numba",
+]
+plotting = [
+ "matplotlib",
+]
+tabor-instruments = [
+ "tabor_control>=0.1.1",
+]
+tektronix = [
+ "tek_awg>=0.2.1",
+]
+tests = [
+ "pytest",
+ "pytest_benchmark",
+]
+zurich-instruments = [
+ "qupulse-hdawg",
+]
+
+[project.urls]
+Homepage = "https://github.com/qutech/qupulse"
+
+[tool.hatch.version]
+path = "qupulse/__init__.py"
+
+[tool.hatch.build.targets.sdist]
+include = [
+ "/qupulse",
+]
+
+[tool.hatch.envs.default]
+features = ["default"]
+
+[tool.hatch.envs.hatch-test]
+template = "default"
+
+[tool.hatch.envs.docs]
+installer = "uv"
+dependencies = [
+ "qupulse[default,zurich-instruments]",
+ "sphinx~=8.1",
+ "nbsphinx~=0.9.6",
+ "sphinx-rtd-theme"
+]
+[tool.hatch.envs.docs.scripts]
+# This is a hack to achieve cross-platform version extraction until https://github.com/pypa/hatch/issues/1006
+build = """
+ python -c "import subprocess, os; \
+ result = subprocess.run(['hatch', 'version'], capture_output=True, text=True); \
+ version = result.stdout.strip(); \
+ subprocess.run(['sphinx-build', '-b', '{args:0}', 'doc/source', 'doc/build/{args:0}', '-d', 'doc/build/.doctrees', '-D', 'version=%s' % version, '-D', 'release=%s' % version])"
+"""
+latex = """
+ python -c "import subprocess, os; \
+ result = subprocess.run(['hatch', 'version'], capture_output=True, text=True); \
+ version = result.stdout.strip(); \
+ subprocess.run(['sphinx-build', '-b', 'latex', 'doc/source', 'doc/build/latex', '-D', 'version=%s' % version, '-D', 'release=%s' % version])"
+"""
+html = """
+ python -c "import subprocess, os; \
+ result = subprocess.run(['hatch', 'version'], capture_output=True, text=True); \
+ version = result.stdout.strip(); \
+ subprocess.run(['sphinx-build', '-b', 'html', 'doc/source', 'doc/build/html', '-D', 'version=%s' % version, '-D', 'release=%s' % version])"
+"""
+clean= """
+python -c "import shutil; shutil.rmtree('doc/build')"
+"""
+clean-notebooks = "jupyter nbconvert --ClearMetadataPreprocessor.enabled=True --to=notebook --execute --inplace --log-level=ERROR doc/source/examples/00*.ipynb doc/source/examples/01*.ipynb doc/source/examples/02*.ipynb doc/source/examples/03*.ipynb"
+
+[tool.hatch.envs.changelog]
+detached = true
+dependencies = [
+ "towncrier",
+]
+
+[tool.hatch.envs.changelog.scripts]
+draft = "towncrier build --version main --draft"
+release = "towncrier build --yes --version {args}"
[tool.towncrier]
directory = "changes.d"
@@ -8,3 +136,21 @@ package_dir = "./qupulse"
filename = "RELEASE_NOTES.rst"
name = "qupulse"
issue_format = "`#{issue} `_"
+
+[tool.pytest.ini_options]
+minversion = "6.0"
+python_files = [
+ "*_tests.py",
+ "*_bug.py"
+]
+filterwarnings = [
+ # syntax is action:message_regex:category:module_regex:lineno
+ # we fail on all with a whitelist because a dependency might mess-up passing the correct stacklevel
+ "error::SyntaxWarning",
+ "error::DeprecationWarning",
+ # pytest uses readline which uses collections.abc
+ # "ignore:Using or importing the ABCs from 'collections' instead of from 'collections\.abc\' is deprecated:DeprecationWarning:.*readline.*"
+]
+
+
+
diff --git a/qctoolkit/__init__.py b/qctoolkit/__init__.py
deleted file mode 100644
index b9a2ab843..000000000
--- a/qctoolkit/__init__.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""This is a (hopefully temporary) alias package to not break existing code. If you know a better way please change"""
-import sys
-import re
-import pkgutil
-import importlib
-import warnings
-import logging
-
-qupulse = importlib.import_module('qupulse')
-sys.modules[__name__] = qupulse
-
-aliased = {}
-
-""" import all subpackages and submodules to assert that
-from qupulse.pulse import TablePT as T1
-from qctoolkit.pulse import TablePT as T2
-assert T1 is T2
-"""
-for _, name, ispkg in pkgutil.walk_packages(qupulse.__path__, 'qupulse.'):
- alias = re.sub('^qupulse.', '%s.' % __name__, name)
-
- try:
- imported = importlib.import_module(name)
- except ImportError:
- warnings.warn('Could not import %s. The alias %s was NOT created.' % (name, alias))
- continue
- sys.modules[alias] = imported
- aliased[alias] = name
-
-logging.info('Created module aliases:', aliased)
diff --git a/qupulse/__init__.py b/qupulse/__init__.py
index 33df6de57..c2da9c3b2 100644
--- a/qupulse/__init__.py
+++ b/qupulse/__init__.py
@@ -1,7 +1,14 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
"""A Quantum compUting PULse parametrization and SEquencing framework."""
-from qupulse.utils.types import MeasurementWindow, ChannelID
-from . import pulses
+import lazy_loader as lazy
+
+__version__ = '0.10'
+
+__getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__)
-__version__ = '0.7'
-__all__ = ["MeasurementWindow", "ChannelID", "pulses"]
+# we explicitly import qupulse to register all deserialization handles
+from qupulse import pulses
diff --git a/qupulse/__init__.pyi b/qupulse/__init__.pyi
new file mode 100644
index 000000000..5a365d4cb
--- /dev/null
+++ b/qupulse/__init__.pyi
@@ -0,0 +1,29 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+from . import pulses
+from . import hardware
+from . import utils
+from . import _program
+from . import program
+
+from . import expressions
+from . import parameter_scope
+from . import serialization
+from . import plotting
+
+from .utils.types import MeasurementWindow, ChannelID
+
+__all__ = ['pulses',
+ 'hardware',
+ 'utils',
+ '_program',
+ 'program',
+ 'expressions',
+ 'parameter_scope',
+ 'serialization',
+ 'MeasurementWindow',
+ 'ChannelID',
+ 'plotting',
+ ]
diff --git a/qupulse/_program/__init__.py b/qupulse/_program/__init__.py
index 93773ebb1..67f60a0be 100644
--- a/qupulse/_program/__init__.py
+++ b/qupulse/_program/__init__.py
@@ -1 +1,8 @@
-"""This is a private package meaning there are no stability guarantees."""
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+"""This is a private package meaning there are no stability guarantees.
+
+Large parts of this package where stabilized and live now in :py:mod:`qupulse.program`.
+"""
diff --git a/qupulse/_program/_loop.py b/qupulse/_program/_loop.py
index 92d3f7eb2..63614a867 100644
--- a/qupulse/_program/_loop.py
+++ b/qupulse/_program/_loop.py
@@ -1,685 +1,13 @@
-from typing import Union, Dict, Iterable, Tuple, cast, List, Optional, Generator, Mapping
-from collections import defaultdict
-from enum import Enum
-import warnings
-import bisect
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
-import numpy as np
-import sympy.ntheory
+"""Backwards compatibility link to qupulse.program.loop"""
-from qupulse._program.waveforms import Waveform, ConstantWaveform
-from qupulse._program.volatile import VolatileRepetitionCount, VolatileProperty
+from qupulse.program.loop import *
-from qupulse.utils import is_integer
-from qupulse.utils.types import TimeType, MeasurementWindow
-from qupulse.utils.tree import Node, is_tree_circular
-from qupulse.utils.numeric import smallest_factor_ge
+import qupulse.program.loop
-from qupulse._program.waveforms import SequenceWaveform, RepetitionWaveform
+__all__ = qupulse.program.loop.__all__
-__all__ = ['Loop', 'make_compatible', 'MakeCompatibleWarning']
-
-
-class Loop(Node):
- MAX_REPR_SIZE = 2000
- __slots__ = ('_waveform', '_measurements', '_repetition_definition', '_cached_body_duration')
-
- """This class represents a initialized (sub-)program as a tree. Each Loop of a valid program has a repetition count
- and either a waveform or a sequence of loops as children.
-
- A Loop can have associated measurements which are also repeated.
- """
- def __init__(self,
- parent: Union['Loop', None] = None,
- children: Iterable['Loop'] = (),
- waveform: Optional[Waveform] = None,
- measurements: Optional[List[MeasurementWindow]] = None,
- repetition_count: Union[int, VolatileRepetitionCount] = 1):
- """Initialize a new loop
-
- Args:
- parent: Forwarded to Node.__init__
- children: Forwarded to Node.__init__
- waveform: "Payload"
- measurements: Associated measurements
- repetition_count: The children / waveform are repeated this often
- """
- super().__init__(parent=parent, children=children)
-
- self._waveform = waveform
- self._measurements = measurements
- self._repetition_definition = repetition_count
- self._cached_body_duration = None
- assert isinstance(repetition_count, VolatileRepetitionCount) or is_integer(repetition_count)
- assert isinstance(waveform, (type(None), Waveform))
-
- def __eq__(self, other: 'Loop') -> bool:
- if type(self) == type(other):
- return (self._repetition_definition == other._repetition_definition and
- self.waveform == other.waveform and
- (self._measurements or None) == (other._measurements or None) and
- len(self) == len(other) and
- all(self_child == other_child for self_child, other_child in zip(self, other)))
- else:
- return NotImplemented
-
- def append_child(self, loop: Optional['Loop'] = None, **kwargs) -> None:
- """Append a child to this loop. Either an existing Loop object or a newly created from kwargs
-
- Args:
- loop: loop to append
- **kwargs: Child is constructed with these kwargs
-
- Raises:
- ValueError: if called with loop and kwargs
- """
- if loop is not None:
- if kwargs:
- raise ValueError("Cannot pass a Loop object and Loop constructor arguments at the same time in "
- "append_child")
- arg = (loop,)
- else:
- arg = (kwargs,)
- super().__setitem__(slice(len(self), len(self)), arg)
- self._invalidate_duration(body_duration_increment=self[-1].duration)
-
- def _invalidate_duration(self, body_duration_increment=None):
- if self._cached_body_duration is not None:
- if body_duration_increment is not None:
- self._cached_body_duration += body_duration_increment
- else:
- self._cached_body_duration = None
- if self.parent:
- if body_duration_increment is not None:
- self.parent._invalidate_duration(body_duration_increment=body_duration_increment*self.repetition_count)
- else:
- self.parent._invalidate_duration()
-
- def add_measurements(self, measurements: Iterable[MeasurementWindow]):
- """Add measurements offset by the current body duration i.e. to the END of the current loop
-
- Args:
- measurements: Measurements to add
- """
- body_duration = float(self.body_duration)
- if body_duration == 0:
- measurements = measurements
- else:
- measurements = ((mw_name, begin+body_duration, length) for mw_name, begin, length in measurements)
-
- if self._measurements is None:
- self._measurements = list(measurements)
- else:
- self._measurements.extend(measurements)
-
- @property
- def waveform(self) -> Waveform:
- return self._waveform
-
- @waveform.setter
- def waveform(self, val) -> None:
- self._waveform = val
- self._invalidate_duration()
-
- @property
- def body_duration(self) -> TimeType:
- if self._cached_body_duration is None:
- if self.is_leaf():
- if self.waveform:
- self._cached_body_duration = self.waveform.duration
- else:
- self._cached_body_duration = TimeType.from_fraction(0, 1)
- else:
- self._cached_body_duration = sum(child.duration for child in self)
- return self._cached_body_duration
-
- @property
- def duration(self) -> TimeType:
- return self.body_duration * TimeType.from_fraction(self.repetition_count, 1)
-
- @property
- def volatile_repetition(self) -> Optional[VolatileProperty]:
- return getattr(self._repetition_definition, 'volatile_property', None)
-
- @property
- def repetition_definition(self) -> Union[int, VolatileRepetitionCount]:
- return self._repetition_definition
-
- @repetition_definition.setter
- def repetition_definition(self, new_definition: Union[int, VolatileRepetitionCount]):
- self._repetition_definition = new_definition
-
- @property
- def repetition_count(self) -> int:
- return int(self._repetition_definition)
-
- @repetition_count.setter
- def repetition_count(self, val: int) -> None:
- assert isinstance(val, (int, float))
- new_repetition = int(val)
- if abs(new_repetition - val) > 1e-10:
- raise ValueError('Repetition count was not an integer')
- self._repetition_definition = new_repetition
-
- def unroll(self) -> None:
- if self.is_leaf():
- raise RuntimeError('Leaves cannot be unrolled')
- if self.volatile_repetition:
- warnings.warn("Unrolling a Loop with volatile repetition count", VolatileModificationWarning)
-
- i = self.parent_index
- self.parent[i:i+1] = (child.copy_tree_structure(new_parent=self.parent)
- for _ in range(self.repetition_count)
- for child in self)
- self.parent.assert_tree_integrity()
-
- def __setitem__(self, idx, value):
- super().__setitem__(idx, value)
- self._invalidate_duration()
-
- def unroll_children(self) -> None:
- if self.volatile_repetition:
- warnings.warn("Unrolling a Loop with volatile repetition count", VolatileModificationWarning)
- old_children = self.children
- self[:] = (child.copy_tree_structure()
- for _ in range(self.repetition_count)
- for child in old_children)
- self.repetition_count = 1
- self.assert_tree_integrity()
-
- def encapsulate(self) -> None:
- """Add a nesting level by moving self to its children."""
- self[:] = [Loop(children=self,
- repetition_count=self._repetition_definition,
- waveform=self._waveform,
- measurements=self._measurements)]
- self.repetition_count = 1
- self._waveform = None
- self._measurements = None
- self.assert_tree_integrity()
-
- def _get_repr(self, first_prefix, other_prefixes) -> Generator[str, None, None]:
- if self.is_leaf():
- yield '%sEXEC %r %d times' % (first_prefix, self._waveform, self.repetition_count)
- else:
- yield '%sLOOP %d times:' % (first_prefix, self.repetition_count)
-
- for elem in self:
- yield from cast(Loop, elem)._get_repr(other_prefixes + ' ->', other_prefixes + ' ')
-
- def __repr__(self) -> str:
- is_circular = is_tree_circular(self)
- if is_circular:
- return '{}: Circ {}'.format(id(self), is_circular)
-
- str_len = 0
- repr_list = []
- for sub_repr in self._get_repr('', ''):
- str_len += len(sub_repr)
-
- if self.MAX_REPR_SIZE and str_len > self.MAX_REPR_SIZE:
- repr_list.append('...')
- break
- else:
- repr_list.append(sub_repr)
- return '\n'.join(repr_list)
-
- def copy_tree_structure(self, new_parent: Union['Loop', bool]=False) -> 'Loop':
- return type(self)(parent=self.parent if new_parent is False else new_parent,
- waveform=self._waveform,
- repetition_count=self._repetition_definition,
- measurements=None if self._measurements is None else list(self._measurements),
- children=(child.copy_tree_structure() for child in self))
-
- def _get_measurement_windows(self, drop: bool) -> Mapping[str, np.ndarray]:
- """Private implementation of get_measurement_windows with a slightly different data format for easier tiling.
-
- Args:
- drop: Drops the measurements from the Loop i.e. the Loop will no longer have measurements attached after
- collecting them
-
- Returns:
- A dictionary (measurement_name -> array) with begin == array[:, 0] and length == array[:, 1]
- """
- temp_meas_windows = defaultdict(list)
- if self._measurements:
- for (mw_name, begin, length) in self._measurements:
- temp_meas_windows[mw_name].append((begin, length))
-
- for mw_name, begin_length_list in temp_meas_windows.items():
- temp_meas_windows[mw_name] = [np.asarray(begin_length_list, dtype=float)]
-
- if drop:
- self._measurements = None
-
- # calculate duration together with meas windows in the same iteration
- if self.is_leaf():
- body_duration = float(self.body_duration)
- else:
- offset = TimeType(0)
- for child in self:
- for mw_name, begins_length_array in child._get_measurement_windows(drop).items():
- begins_length_array[:, 0] += float(offset)
- temp_meas_windows[mw_name].append(begins_length_array)
- offset += child.duration
-
- body_duration = float(offset)
-
- # formatting like this for easier debugging
- result = {}
-
- # repeat and add repetition based offset
- for mw_name, begin_length_list in temp_meas_windows.items():
- result[mw_name] = _repeat_loop_measurements(begin_length_list, self.repetition_count, body_duration)
-
- return result
-
- def get_measurement_windows(self, drop=False) -> Dict[str, Tuple[np.ndarray, np.ndarray]]:
- """Iterates over all children and collect the begin and length arrays of each measurement window.
-
- Args:
- drop: Drops the measurements from the Loop i.e. the Loop will no longer have measurements attached after
- collecting them.
-
- Returns:
- A dictionary (measurement_name -> (begin, length)) with begin and length being :class:`numpy.ndarray`
- """
- return {mw_name: (begin_length_list[:, 0], begin_length_list[:, 1])
- for mw_name, begin_length_list in self._get_measurement_windows(drop=drop).items()}
-
- def split_one_child(self, child_index=None) -> None:
- """Take the last child that has a repetition count larger one, decrease it's repetition count and insert a copy
- with repetition cout one after it"""
- if child_index is not None:
- if self[child_index].repetition_count < 2:
- raise ValueError('Cannot split child {} as the repetition count is not larger 1')
-
- else:
- # we cannot reverse enumerate
- n_child = len(self) - 1
- for reverse_idx, child in enumerate(reversed(self)):
- if child.repetition_count > 1:
- forward_idx = n_child - reverse_idx
- if not child.volatile_repetition:
- child_index = forward_idx
- break
- elif child_index is None:
- child_index = forward_idx
- else:
- if child_index is None:
- raise RuntimeError('There is no child with repetition count > 1')
-
- if self[child_index].volatile_repetition:
- warnings.warn("Splitting a child with volatile repetition count", VolatileModificationWarning)
-
- new_child = self[child_index].copy_tree_structure()
- new_child.repetition_count = 1
-
- self[child_index].repetition_count -= 1
-
- self[child_index+1:child_index+1] = (new_child,)
- self.assert_tree_integrity()
-
- def flatten_and_balance(self, depth: int) -> None:
- """Modifies the program so all tree branches have the same depth.
-
- Args:
- depth: Target depth of the program
- """
- i = 0
- while i < len(self):
- # only used by type checker
- sub_program = cast(Loop, self[i])
-
- if sub_program.depth() < depth - 1:
- # increase nesting because the subprogram is not deep enough
- sub_program.encapsulate()
-
- elif not sub_program.is_balanced():
- # balance the sub program. We revisit it in the next iteration (no change of i )
- # because it might modify self. While writing this comment I am not sure this is true. 14.01.2020 Simon
- sub_program.flatten_and_balance(depth - 1)
-
- elif sub_program.depth() == depth - 1:
- # subprogram is balanced with the correct depth
- i += 1
-
- elif sub_program._has_single_child_that_can_be_merged():
- # subprogram is balanced but to deep and has no measurements -> we can "lift" the sub-sub-program
- # TODO: There was a len(sub_sub_program) == 1 check here that I cannot explain
- sub_program._merge_single_child()
-
- elif not sub_program.is_leaf():
- # subprogram is balanced but too deep
- sub_program.unroll()
-
- else:
- # we land in this case if the function gets called with depth == 0 and the current subprogram is a leaf
- i += 1
-
- def _has_single_child_that_can_be_merged(self) -> bool:
- if len(self) == 1:
- child = cast(Loop, self[0])
- return not self._measurements or (child.repetition_count == 1 and not child.volatile_repetition)
- else:
- return False
-
- def _merge_single_child(self):
- """Lift the single child to current level. Requires _has_single_child_that_can_be_merged to be true"""
- assert len(self) == 1, "bug: _merge_single_child called on loop with len != 1"
- child = cast(Loop, self[0])
-
- # if the child has a fixed repetition count of 1 the measurements can be merged
- mergable_measurements = child.repetition_count == 1 and not child.volatile_repetition
-
- assert not self._measurements or mergable_measurements, "bug: _merge_single_child called on loop with measurements"
- assert not self._waveform, "bug: _merge_single_child called on loop with children and waveform"
-
- measurements = child._measurements
- if self._measurements:
- if measurements:
- measurements.extend(self._measurements)
- else:
- measurements = self._measurements
-
- if not self.volatile_repetition and not child.volatile_repetition:
- # simple integer multiplication
- repetition_definition = self.repetition_count * child.repetition_count
- elif not self.volatile_repetition:
- repetition_definition = child._repetition_definition * self.repetition_count
- elif not child.volatile_repetition:
- repetition_definition = self._repetition_definition * child.repetition_count
- else:
- # create a new expression that depends on both
- expression = 'parent_repetition_count * child_repetition_count'
- repetition_definition = VolatileRepetitionCount.operation(
- expression=expression,
- parent_repetition_count=self._repetition_definition,
- child_repetition_count=child._repetition_definition)
-
- self[:] = iter(child)
- self._waveform = child._waveform
- self._repetition_definition = repetition_definition
- self._measurements = measurements
- self._invalidate_duration()
- return True
-
- def cleanup(self, actions=('remove_empty_loops', 'merge_single_child')):
- """Apply the specified actions to cleanup the Loop.
-
- remove_empty_loops: Remove loops with no children and no waveform (a DroppedMeasurementWarning is issued)
- merge_single_child: see `_try_merge_single_child` documentation
-
- Warnings:
- DroppedMeasurementWarning: Likely a bug in qupulse. TODO: investigate whether there are usecases
- """
- if 'remove_empty_loops' in actions:
- new_children = []
- for child in self:
- child = cast(Loop, child)
- if child.is_leaf():
- if child.waveform is None:
- if child._measurements:
- warnings.warn("Dropping measurement since there is no waveform attached",
- category=DroppedMeasurementWarning)
- else:
- new_children.append(child)
-
- else:
- child.cleanup(actions)
- if child.waveform or not child.is_leaf():
- new_children.append(child)
-
- elif child._measurements:
- warnings.warn("Dropping measurement since there is no waveform in children",
- category=DroppedMeasurementWarning)
-
- if len(self) != len(new_children):
- self[:] = new_children
-
- else:
- # only do the recursive call
- for child in self:
- child.cleanup(actions)
-
- if 'merge_single_child' in actions and self._has_single_child_that_can_be_merged():
- self._merge_single_child()
-
- def get_duration_structure(self) -> Tuple[int, Union[TimeType, tuple]]:
- if self.is_leaf():
- return self.repetition_count, self.waveform.duration
- else:
- return self.repetition_count, tuple(child.get_duration_structure() for child in self)
-
- def reverse_inplace(self):
- if self.is_leaf():
- self._waveform = self._waveform.reversed()
- else:
- self._reverse_children()
- for child in self:
- child.reverse_inplace()
- if self._measurements:
- duration = self.duration
- self._measurements = [
- (name, duration - (begin + length), length)
- for name, begin, length in self._measurements
- ]
-
-
-class ChannelSplit(Exception):
- def __init__(self, channel_sets):
- self.channel_sets = channel_sets
-
-
-def to_waveform(program: Loop) -> Waveform:
- if program.is_leaf():
- if program.repetition_count == 1:
- return program.waveform
- else:
- return RepetitionWaveform.from_repetition_count(program.waveform, program.repetition_count)
- else:
- if len(program) == 1:
- sequenced_waveform = to_waveform(cast(Loop, program[0]))
- else:
- sequenced_waveform = SequenceWaveform.from_sequence(
- [to_waveform(cast(Loop, sub_program))
- for sub_program in program])
- if program.repetition_count > 1:
- return RepetitionWaveform.from_repetition_count(sequenced_waveform, program.repetition_count)
- else:
- return sequenced_waveform
-
-
-class _CompatibilityLevel(Enum):
- compatible = 0
- action_required = 1
- incompatible_too_short = 2
- incompatible_fraction = 3
- incompatible_quantum = 4
-
- def is_incompatible(self) -> bool:
- return self in (self.incompatible_fraction, self.incompatible_quantum, self.incompatible_too_short)
-
-
-def _is_compatible(program: Loop, min_len: int, quantum: int, sample_rate: TimeType) -> _CompatibilityLevel:
- """ check whether program loop is compatible with awg requirements
- possible reasons for incompatibility:
- program shorter than minimum length
- program duration not an integer
- program duration not a multiple of quantum """
- program_duration_in_samples = program.duration * sample_rate
-
- if program_duration_in_samples.denominator != 1:
- return _CompatibilityLevel.incompatible_fraction
-
- if program_duration_in_samples < min_len:
- return _CompatibilityLevel.incompatible_too_short
-
- if program_duration_in_samples % quantum > 0:
- return _CompatibilityLevel.incompatible_quantum
-
- if program.is_leaf():
- waveform_duration_in_samples = program.body_duration * sample_rate
- if waveform_duration_in_samples < min_len or (waveform_duration_in_samples / quantum).denominator != 1:
- if program.volatile_repetition:
- warnings.warn("_is_compatible requires an action which drops volatility.",
- category=VolatileModificationWarning)
- return _CompatibilityLevel.action_required
- else:
- return _CompatibilityLevel.compatible
- else:
- if all(_is_compatible(cast(Loop, sub_program), min_len, quantum, sample_rate) == _CompatibilityLevel.compatible
- for sub_program in program):
- return _CompatibilityLevel.compatible
- else:
- if program.volatile_repetition:
- warnings.warn("_is_compatible requires an action which drops volatility.",
- category=VolatileModificationWarning)
- return _CompatibilityLevel.action_required
-
-
-def _make_compatible(program: Loop, min_len: int, quantum: int, sample_rate: TimeType) -> None:
- if program.is_leaf():
- program.waveform = to_waveform(program.copy_tree_structure())
- program.repetition_count = 1
- else:
- comp_levels = [_is_compatible(cast(Loop, sub_program), min_len, quantum, sample_rate)
- for sub_program in program]
-
- if any(comp_level.is_incompatible() for comp_level in comp_levels):
- single_run = program.duration * sample_rate / program.repetition_count
- if (single_run / quantum).denominator == 1 and single_run >= min_len:
- # it is enough to concatenate all children
- new_repetition_definition = program.repetition_definition
- program.repetition_count = 1
- else:
- # we need to concatenate all children and unroll
- new_repetition_definition = 1
-
- program.waveform = to_waveform(program.copy_tree_structure())
- program.repetition_definition = new_repetition_definition
- program[:] = []
- return
- else:
- for sub_program, comp_level in zip(program, comp_levels):
- if comp_level == _CompatibilityLevel.action_required:
- _make_compatible(sub_program, min_len, quantum, sample_rate)
-
-
-def make_compatible(program: Loop, minimal_waveform_length: int, waveform_quantum: int, sample_rate: TimeType):
- """ check program for compatibility to AWG requirements, make it compatible if necessary and possible"""
- comp_level = _is_compatible(program,
- min_len=minimal_waveform_length,
- quantum=waveform_quantum,
- sample_rate=sample_rate)
- if comp_level == _CompatibilityLevel.incompatible_fraction:
- raise ValueError('The program duration in samples {} is not an integer'.format(program.duration * sample_rate))
- if comp_level == _CompatibilityLevel.incompatible_too_short:
- raise ValueError('The program is too short to be a valid waveform. \n'
- ' program duration in samples: {} \n'
- ' minimal length: {}'.format(program.duration * sample_rate, minimal_waveform_length))
- if comp_level == _CompatibilityLevel.incompatible_quantum:
- raise ValueError('The program duration in samples {} '
- 'is not a multiple of quantum {}'.format(program.duration * sample_rate, waveform_quantum))
-
- elif comp_level == _CompatibilityLevel.action_required:
- warnings.warn("qupulse will now concatenate waveforms to make the pulse/program compatible with the chosen AWG."
- " This might take some time. If you need this pulse more often it makes sense to write it in a "
- "way which is more AWG friendly.", MakeCompatibleWarning)
-
- _make_compatible(program,
- min_len=minimal_waveform_length,
- quantum=waveform_quantum,
- sample_rate=sample_rate)
-
- else:
- assert comp_level == _CompatibilityLevel.compatible
-
-
-def roll_constant_waveforms(program: Loop, minimal_waveform_quanta: int, waveform_quantum: int, sample_rate: TimeType):
- """This function finds waveforms in program that can be replaced with repetitions of shorter waveforms and replaces
- them. Complexity O(N_waveforms). Drops measurements because they are not correctly handled here for performance
- reasons.
-
- This is possible if:
- - The waveform is constant on all channels
- - waveform.duration * sample_rate / waveform_quantum has a factor that is bigger than minimal_waveform_quanta
-
- Args:
- program:
- minimal_waveform_quanta:
- waveform_quantum:
- sample_rate:
-
- Warnings:
- DroppedMeasurementWarning: This warning is raised if a measurement is dropped.
- """
- if program._measurements:
- warnings.warn("Dropping measurements. Remove measurements before calling roll_constant_waveforms by calling"
- " get_measurement_windows(drop=True).", category=DroppedMeasurementWarning)
- program._measurements = None
-
- waveform = program.waveform
-
- if waveform is None:
- for child in program:
- roll_constant_waveforms(child, minimal_waveform_quanta, waveform_quantum, sample_rate)
- else:
- waveform_quanta = (waveform.duration * sample_rate) // waveform_quantum
-
- # example
- # waveform_quanta = 15
- # minimal_waveform_quanta = 2
- # => repetition_count = 5, new_waveform_quanta = 3
- if waveform_quanta < minimal_waveform_quanta * 2:
- # there is no way to roll this waveform because it is too short
- return
-
- const_values = waveform.constant_value_dict()
- if const_values is None:
- # The waveform is not constant
- return
-
- new_waveform_quanta = smallest_factor_ge(waveform_quanta, min_factor=minimal_waveform_quanta)
- if new_waveform_quanta == waveform_quanta:
- # the waveform duration in samples has no suitable factor
- # TODO: Option to insert multiple Loop objects
- return
-
- additional_repetition_count = waveform_quanta // new_waveform_quanta
-
- new_waveform = ConstantWaveform.from_mapping(
- duration=waveform_quantum * new_waveform_quanta / sample_rate,
- constant_values=const_values)
-
- # use the private properties to avoid invalidating the duration cache of the parent loop
- program._repetition_definition = program.repetition_definition * additional_repetition_count
- program._waveform = new_waveform
-
-
-def _repeat_loop_measurements(begin_length_list: List[np.ndarray],
- repetition_count: int,
- body_duration: float
- ) -> np.ndarray:
- temp_begin_length_array = np.concatenate(begin_length_list)
-
- begin_length_array = np.tile(temp_begin_length_array, (repetition_count, 1))
-
- shaped_begin_length_array = np.reshape(begin_length_array, (repetition_count, -1, 2))
-
- shaped_begin_length_array[:, :, 0] += (np.arange(repetition_count) * body_duration)[:, np.newaxis]
-
- return begin_length_array
-
-
-class MakeCompatibleWarning(ResourceWarning):
- pass
-
-
-class VolatileModificationWarning(RuntimeWarning):
- """This warning is emitted if the colatile part of a program gets modified. This might imply that the volatile
- parameter cannot be change anymore."""
-
-
-class DroppedMeasurementWarning(RuntimeWarning):
- """This warning is emitted if a measurement was dropped because there was no waveform attached."""
+del qupulse
diff --git a/qupulse/_program/seqc.py b/qupulse/_program/seqc.py
deleted file mode 100644
index 50dd29a7c..000000000
--- a/qupulse/_program/seqc.py
+++ /dev/null
@@ -1,1498 +0,0 @@
-"""This module contains the ZI HDAWG compatible description of programs. There is no code in here that interacts with
-hardware directly.
-
-The public interface to all functionality is given by `HDAWGProgramManager`. This class can create seqc source code
-which contains multiple programs and allows switching between these with the user registers of a device,
-
-Furthermore:
-- `SEQCNode`: AST of a subset of sequencing C
-- `loop_to_seqc`: conversion of `Loop` objects to this subset in a clever way
-- `BinaryWaveform`: Bundles functionality of handling segments in a native way.
-- `WaveformMemory`: Functionality to sync waveforms to the device (via the LabOne user folder)
-- `ProgramWaveformManager` and `HDAWGProgramEntry`: Program wise handling of waveforms and seqc-code
-classes that convert `Loop` objects"""
-import warnings
-from typing import Optional, Union, Sequence, Dict, Iterator, Tuple, Callable, NamedTuple, MutableMapping, Mapping,\
- Iterable, Any, List, Deque
-from types import MappingProxyType
-import abc
-import itertools
-import inspect
-import logging
-import hashlib
-from weakref import WeakValueDictionary
-from collections import OrderedDict
-import re
-import collections
-import numbers
-import string
-import functools
-
-import numpy as np
-from pathlib import Path
-
-from qupulse.utils.types import ChannelID, TimeType
-from qupulse.utils import replace_multiple, grouper
-from qupulse._program.waveforms import Waveform
-from qupulse._program._loop import Loop
-from qupulse._program.volatile import VolatileRepetitionCount, VolatileProperty
-from qupulse.hardware.awgs.base import ProgramEntry
-from qupulse.hardware.util import zhinst_voltage_to_uint16
-
-try:
- # zhinst fires a DeprecationWarning from its own code in some versions...
- with warnings.catch_warnings():
- warnings.simplefilter('ignore', DeprecationWarning)
- import zhinst.utils
-except ImportError:
- zhinst = None
-
-
-__all__ = ["HDAWGProgramManager"]
-
-
-def make_valid_identifier(name: str) -> str:
- # replace all invalid characters and conactenate with hash of original name
- name_hash = hashlib.sha256(name.encode('utf-8')).hexdigest()
- valid_chars = string.ascii_letters + string.digits + '_'
- namestub = ''.join(c for c in name if c in valid_chars)
- return f'renamed_{namestub}_{name_hash}'
-
-
-class BinaryWaveform:
- """This class represents a sampled waveform in the native HDAWG format as returned
- by zhinst.utils.convert_awg_waveform.
-
- BinaryWaveform.data can be uploaded directly to {device]/awgs/{awg}/waveform/waves/{wf}
-
- `to_csv_compatible_table` can be used to create a compatible compact csv file (with marker data included)
- """
- __slots__ = ('data',)
-
- PLAYBACK_QUANTUM = 16
- PLAYBACK_MIN_QUANTA = 2
-
- def __init__(self, data: np.ndarray):
- """ TODO: always use both channels?
-
- Args:
- data: data as returned from zhinst.utils.convert_awg_waveform
- """
- n_quantum, remainder = divmod(data.size, 3 * self.PLAYBACK_QUANTUM)
- assert n_quantum > 1, "Waveform too short (min len is 32)"
- assert remainder == 0, "Waveform has not a valid length"
- assert data.dtype is np.dtype('uint16')
- assert np.all(data[2::3] < 16), "invalid marker data"
- assert data.ndim == 1, "Data not one dimensional"
-
- self.data = data
- self.data.flags.writeable = False
-
- @property
- def ch1(self):
- return self.data[::3]
-
- @property
- def ch2(self):
- return self.data[1::3]
-
- @property
- def marker_data(self):
- return self.data[2::3]
-
- @property
- def markers_ch1(self):
- return np.bitwise_and(self.marker_data, 0b0011)
-
- @property
- def markers_ch2(self):
- return np.bitwise_and(self.marker_data, 0b1100)
-
- @classmethod
- def from_sampled(cls, ch1: Optional[np.ndarray], ch2: Optional[np.ndarray],
- markers: Tuple[Optional[np.ndarray], Optional[np.ndarray],
- Optional[np.ndarray], Optional[np.ndarray]]) -> 'BinaryWaveform':
- """Combines the sampled and scaled waveform data into a single binary compatible waveform
-
- Args:
- ch1: sampled waveform scaled to full range (-1., 1.)
- ch2: sampled waveform scaled to full range (-1., 1.)
- markers: (ch1_front_marker, ch1_dio_marker, ch2_front_marker, ch2_dio_marker)
-
- Returns:
-
- """
- return cls(zhinst_voltage_to_uint16(ch1, ch2, markers))
-
- @classmethod
- def zeroed(cls, size):
- return cls(zhinst.utils.convert_awg_waveform(np.zeros(size), np.zeros(size), np.zeros(size, dtype=np.uint16)))
-
- def __len__(self):
- return self.data.size // 3
-
- def __eq__(self, other):
- return np.array_equal(self.data, other.data)
-
- def __hash__(self):
- return hash(bytes(self.data))
-
- def fingerprint(self) -> str:
- """This fingerprint is runtime independent"""
- return hashlib.sha256(self.data).hexdigest()
-
- def to_csv_compatible_table(self) -> np.ndarray:
- """The integer values in that file should be 18-bit unsigned integers with the two least significant bits
- being the markers. The values are mapped to 0 => -FS, 262143 => +FS, with FS equal to the full scale.
-
- >>> np.savetxt(waveform_dir, binary_waveform.to_csv_compatible_table(), fmt='%u')
- """
- assert self.data.size % self.PLAYBACK_QUANTUM == 0, "conversion to csv requires a valid length"
-
- table = np.zeros((len(self), 2), dtype=np.uint32)
- table[:, 0] = self.ch1
- table[:, 1] = self.ch2
- np.left_shift(table, 2, out=table)
- table[:, 0] += self.markers_ch1
- table[:, 1] += self.markers_ch2
-
- return table
-
- def playback_possible(self) -> bool:
- """Returns if the waveform can be played without padding"""
- return self.data.size % self.PLAYBACK_QUANTUM == 0
-
- def dynamic_rate(self, max_rate: int = 12) -> int:
- min_pre_division_quanta = 2 * self.PLAYBACK_QUANTUM
-
- reduced = self.data.reshape(-1, 3)
- for n in range(max_rate):
- n_quantum, remainder = divmod(reduced.shape[0], min_pre_division_quanta)
- if remainder != 0 or n_quantum < self.PLAYBACK_MIN_QUANTA or np.any(reduced[::2, :] != reduced[1::2, :]):
- return n
- reduced = reduced[::2, :]
- return max_rate
-
-
-class ConcatenatedWaveform:
- def __init__(self):
- """Handle the concatenation of multiple binary waveforms to create a big indexable waveform."""
- self._concatenated: Optional[List[Tuple[BinaryWaveform, ...]]] = []
- self._as_binary: Optional[Tuple[BinaryWaveform, ...]] = None
-
- def __bool__(self):
- return bool(self._concatenated)
-
- def is_finalized(self):
- return self._as_binary is not None or self._concatenated is None
-
- def as_binary(self) -> Optional[Tuple[BinaryWaveform, ...]]:
- assert self.is_finalized()
- return self._as_binary
-
- def append(self, binary_waveform: Tuple[BinaryWaveform, ...]):
- assert not self.is_finalized()
- assert not self._concatenated or len(self._concatenated[-1]) == len(binary_waveform)
- self._concatenated.append(binary_waveform)
-
- def finalize(self):
- assert not self.is_finalized()
- if self._concatenated:
- n_groups = len(self._concatenated[0])
- as_binary = [[] for _ in range(n_groups)]
- for wf_tuple in self._concatenated:
- for grp, wf in enumerate(wf_tuple):
- as_binary[grp].append(wf.data)
- self._as_binary = tuple(BinaryWaveform(np.concatenate(as_bin)) for as_bin in as_binary)
- else:
- self._concatenated = None
-
- def clear(self):
- if self._concatenated is None:
- self._concatenated = []
- else:
- self._concatenated.clear()
- self._as_binary = None
-
-
-class WaveformFileSystem:
- logger = logging.getLogger('qupulse.hdawg.waveforms')
- _by_path = WeakValueDictionary()
-
- def __init__(self, path: Path):
- """This class coordinates multiple AWGs (channel pairs) using the same file system to store the waveforms.
-
- Args:
- path: Waveforms are stored here
- """
- self._required = {}
- self._path = path
-
- @classmethod
- def get_waveform_file_system(cls, path: Path) -> 'WaveformFileSystem':
- """Get the instance for the given path. Multiple instances that access the same path lead to inconsistencies."""
- return cls._by_path.setdefault(path, cls(path))
-
- def sync(self, client: 'WaveformMemory', waveforms: Mapping[str, BinaryWaveform], **kwargs):
- """Write the required waveforms to the filesystem."""
- self._required[id(client)] = waveforms
- self._sync(**kwargs)
-
- def _sync(self, delete=True, write_all=False):
- to_save = {self._path.joinpath(file_name): binary
- for d in self._required.values()
- for file_name, binary in d.items()}
-
- for existing_file in self._path.iterdir():
- if not existing_file.is_file():
- pass
- elif existing_file in to_save:
- if not write_all:
- self.logger.debug('Skipping %r', existing_file.name)
- to_save.pop(existing_file)
- elif delete:
- try:
- self.logger.debug('Deleting %r', existing_file.name)
- existing_file.unlink()
- except OSError:
- self.logger.exception("Error deleting: %r", existing_file.name)
-
- for file_name, binary_waveform in to_save.items():
- table = binary_waveform.to_csv_compatible_table()
- np.savetxt(file_name, table, '%u')
- self.logger.debug('Wrote %r', file_name)
-
-
-class WaveformMemory:
- """Global waveform "memory" representation (currently the file system)"""
- CONCATENATED_WAVEFORM_TEMPLATE = '{program_name}_concatenated_waveform_{group_index}'
- SHARED_WAVEFORM_TEMPLATE = '{program_name}_shared_waveform_{hash}'
- WF_PLACEHOLDER_TEMPLATE = '*{id}*'
- FILE_NAME_TEMPLATE = '{hash}.csv'
-
- _WaveInfo = NamedTuple('_WaveInfo', [('wave_name', str),
- ('file_name', str),
- ('binary_waveform', BinaryWaveform)])
-
- def __init__(self):
- self.shared_waveforms = OrderedDict() # type: MutableMapping[BinaryWaveform, set]
- self.concatenated_waveforms = OrderedDict() # type: MutableMapping[str, ConcatenatedWaveform]
-
- def clear(self):
- self.shared_waveforms.clear()
- self.concatenated_waveforms.clear()
-
- def _shared_waveforms_iter(self) -> Iterator[Tuple[str, _WaveInfo]]:
- for wf, program_set in self.shared_waveforms.items():
- if program_set:
- wave_hash = wf.fingerprint()
- wave_name = self.SHARED_WAVEFORM_TEMPLATE.format(program_name='_'.join(program_set),
- hash=wave_hash)
- wave_placeholder = self.WF_PLACEHOLDER_TEMPLATE.format(id=id(program_set))
- file_name = self.FILE_NAME_TEMPLATE.format(hash=wave_hash)
- yield wave_placeholder, self._WaveInfo(wave_name, file_name, wf)
-
- def _concatenated_waveforms_iter(self) -> Iterator[Tuple[str, Tuple[_WaveInfo, ...]]]:
- for program_name, concatenated_waveform in self.concatenated_waveforms.items():
- # we assume that if the first entry is not empty the rest also isn't
- if concatenated_waveform:
- infos = []
- for group_index, binary in enumerate(concatenated_waveform.as_binary()):
- wave_hash = binary.fingerprint()
- wave_name = self.CONCATENATED_WAVEFORM_TEMPLATE.format(program_name=program_name,
- group_index=group_index)
- file_name = self.FILE_NAME_TEMPLATE.format(hash=wave_hash)
- infos.append(self._WaveInfo(wave_name, file_name, binary))
-
- wave_placeholder = self.WF_PLACEHOLDER_TEMPLATE.format(id=id(concatenated_waveform))
- yield wave_placeholder, tuple(infos)
-
- def _all_info_iter(self) -> Iterator[_WaveInfo]:
- for _, infos in self._concatenated_waveforms_iter():
- yield from infos
- for _, info in self._shared_waveforms_iter():
- yield info
-
- def waveform_name_replacements(self) -> Dict[str, str]:
- """replace place holders of complete seqc program with
-
- >>> waveform_name_translation = waveform_memory.waveform_name_replacements()
- >>> seqc_program = qupulse.utils.replace_multiple(seqc_program, waveform_name_translation)
- """
- translation = {}
- for wave_placeholder, wave_info in self._shared_waveforms_iter():
- translation[wave_placeholder] = wave_info.wave_name
-
- for wave_placeholder, wave_infos in self._concatenated_waveforms_iter():
- translation[wave_placeholder] = ','.join(info.wave_name for info in wave_infos)
- return translation
-
- def waveform_declaration(self) -> str:
- """Produces a string that declares all needed waveforms.
- It is needed to know the waveform index in case we want to update a waveform during playback."""
- declarations = []
- for wave_info in self._all_info_iter():
- declarations.append(
- 'wave {wave_name} = "{file_name}";'.format(wave_name=wave_info.wave_name,
- file_name=wave_info.file_name.replace('.csv', ''))
- )
- return '\n'.join(declarations)
-
- def sync_to_file_system(self, file_system: WaveformFileSystem):
- to_save = {wave_info.file_name: wave_info.binary_waveform
- for wave_info in self._all_info_iter()}
- file_system.sync(self, to_save)
-
-
-class ProgramWaveformManager:
- """Manages waveforms of a program"""
- def __init__(self, name: str, memory: WaveformMemory):
- if not name.isidentifier():
- waveform_name = make_valid_identifier(name)
- else:
- waveform_name = name
-
- self._waveform_name = waveform_name
- self._program_name = name
- self._memory = memory
-
- assert self._program_name not in self._memory.concatenated_waveforms
- assert all(self._program_name not in programs for programs in self._memory.shared_waveforms.values())
- self._memory.concatenated_waveforms[waveform_name] = ConcatenatedWaveform()
-
- @property
- def program_name(self) -> str:
- return self._program_name
-
- @property
- def main_waveform_name(self) -> str:
- self._waveform_name
-
- def clear_requested(self):
- for programs in self._memory.shared_waveforms.values():
- programs.discard(self._program_name)
- self._memory.concatenated_waveforms[self._waveform_name].clear()
-
- def request_shared(self, binary_waveform: Tuple[BinaryWaveform, ...]) -> str:
- """Register waveform if not already registered and return a unique identifier placeholder.
-
- The unique identifier currently is computed from the id of the set which stores all programs using this
- waveform.
- """
- placeholders = []
- for wf in binary_waveform:
- program_set = self._memory.shared_waveforms.setdefault(wf, set())
- program_set.add(self._program_name)
- placeholders.append(self._memory.WF_PLACEHOLDER_TEMPLATE.format(id=id(program_set)))
- return ",".join(placeholders)
-
- def request_concatenated(self, binary_waveform: Tuple[BinaryWaveform, ...]) -> str:
- """Append the waveform to the concatenated waveform"""
- bin_wf_list = self._memory.concatenated_waveforms[self._waveform_name]
- bin_wf_list.append(binary_waveform)
- return self._memory.WF_PLACEHOLDER_TEMPLATE.format(id=id(bin_wf_list))
-
- def finalize(self):
- self._memory.concatenated_waveforms[self._waveform_name].finalize()
-
- def prepare_delete(self):
- """Delete all references in waveform memory to this program. Cannot be used afterwards."""
- self.clear_requested()
- del self._memory.concatenated_waveforms[self._waveform_name]
-
-
-class UserRegister:
- """This class is a helper class to avoid errors due to 0 and 1 based register indexing"""
- __slots__ = ('_zero_based_value',)
-
- def __init__(self, *, zero_based_value: int = None, one_based_value: int = None):
- assert None in (zero_based_value, one_based_value)
- assert isinstance(zero_based_value, int) or isinstance(one_based_value, int)
-
- if one_based_value is not None:
- assert one_based_value > 0, "A one based value needs to be larger zero"
- self._zero_based_value = one_based_value - 1
- else:
- self._zero_based_value = zero_based_value
-
- @classmethod
- def from_seqc(cls, value: int) -> 'UserRegister':
- return cls(zero_based_value=value)
-
- def to_seqc(self) -> int:
- return self._zero_based_value
-
- @classmethod
- def from_labone(cls, value: int) -> 'UserRegister':
- return cls(zero_based_value=value)
-
- def to_labone(self) -> int:
- return self._zero_based_value
-
- @classmethod
- def from_web_interface(cls, value: int) -> 'UserRegister':
- return cls(one_based_value=value)
-
- def to_web_interface(self) -> int:
- return self._zero_based_value + 1
-
- def __hash__(self):
- return hash(self._zero_based_value)
-
- def __eq__(self, other):
- return self._zero_based_value == getattr(other, '_zero_based_value', None)
-
- def __repr__(self):
- return 'UserRegister(zero_based_value={zero_based_value})'.format(zero_based_value=self._zero_based_value)
-
- def __format__(self, format_spec: str) -> str:
- if format_spec in ('zero_based', 'seqc', 'labone', 'lab_one'):
- return str(self.to_seqc())
- elif format_spec in ('one_based', 'web', 'web_interface'):
- return str(self.to_web_interface())
- elif format_spec in ('repr', 'r'):
- return repr(self)
- else:
- raise ValueError('Invalid format spec for UserRegister: ', format_spec)
-
-
-class UserRegisterManager:
- """This class keeps track of the user registered that are used in a certain context"""
- def __init__(self, available: Iterable[UserRegister], name_template: str):
- assert 'register' in (x[1] for x in string.Formatter().parse(name_template))
-
- self._available = set(available)
- self._name_template = name_template
- self._used = {}
-
- def request(self, obj) -> str:
- """Request a user register name to store object. If an object that evaluates equal to obj was requested before
- the name name is returned.
-
- Args:
- obj: Object to store
-
- Returns:
- Name of the variable with the user register
-
- Raises:
- Value error if no register is available
- """
- for register, registered_obj in self._used.items():
- if obj == registered_obj:
- return self._name_template.format(register=register)
- if self._available:
- register = self._available.pop()
- self._used[register] = obj
- return self._name_template.format(register=register)
- else:
- raise ValueError("No register available for %r" % obj)
-
- def iter_used_register_names(self) -> Iterator[Tuple[UserRegister, str]]:
- """
-
- Returns:
- An iterator over (register index, register name) pairs
- """
- return ((register, self._name_template.format(register=register)) for register in self._used.keys())
-
- def iter_used_register_values(self) -> Iterable[Tuple[UserRegister, Any]]:
- return self._used.items()
-
-
-class HDAWGProgramEntry(ProgramEntry):
- USER_REG_NAME_TEMPLATE = 'user_reg_{register:seqc}'
-
- def __init__(self, loop: Loop, selection_index: int, waveform_memory: WaveformMemory, program_name: str,
- channels: Tuple[Optional[ChannelID], ...],
- markers: Tuple[Optional[ChannelID], ...],
- amplitudes: Tuple[float, ...],
- offsets: Tuple[float, ...],
- voltage_transformations: Tuple[Optional[Callable], ...],
- sample_rate: TimeType):
- super().__init__(loop, channels=channels, markers=markers,
- amplitudes=amplitudes,
- offsets=offsets,
- voltage_transformations=voltage_transformations,
- sample_rate=sample_rate)
- for waveform, (all_sampled_channels, all_sampled_markers) in self._waveforms.items():
- size = int(waveform.duration * sample_rate)
-
- # group in channel pairs for binary waveform
- binary_waveforms = []
- for (sampled_channels, sampled_markers) in zip(grouper(all_sampled_channels, 2),
- grouper(all_sampled_markers, 4)):
- if all(x is None for x in (*sampled_channels, *sampled_markers)):
- # empty channel pairs
- binary_waveforms.append(BinaryWaveform.zeroed(size))
- else:
- binary_waveforms.append(BinaryWaveform.from_sampled(*sampled_channels, sampled_markers))
- self._waveforms[waveform] = tuple(binary_waveforms)
-
- self._waveform_manager = ProgramWaveformManager(program_name, waveform_memory)
- self.selection_index = selection_index
- self._trigger_wait_code = None
- self._seqc_node = None
- self._seqc_source = None
- self._var_declarations = None
- self._user_registers = None
- self._user_register_source = None
-
- def compile(self,
- min_repetitions_for_for_loop: int,
- min_repetitions_for_shared_wf: int,
- indentation: str,
- trigger_wait_code: str,
- available_registers: Iterable[UserRegister]):
- """Compile the loop representation to an internal sequencing c one using `loop_to_seqc`
-
- Args:
- min_repetitions_for_for_loop: See `loop_to_seqc`
- min_repetitions_for_shared_wf: See `loop_to_seqc`
- indentation: Each line is prefixed with this
- trigger_wait_code: The code is put before the playback start
- available_registers
- Returns:
-
- """
- pos_var_name = 'pos'
-
- if self._seqc_node:
- self._waveform_manager.clear_requested()
-
- user_registers = UserRegisterManager(available_registers, self.USER_REG_NAME_TEMPLATE)
-
- self._seqc_node = loop_to_seqc(self._loop,
- min_repetitions_for_for_loop=min_repetitions_for_for_loop,
- min_repetitions_for_shared_wf=min_repetitions_for_shared_wf,
- waveform_to_bin=self.get_binary_waveform,
- user_registers=user_registers)
-
- self._user_register_source = '\n'.join(
- '{indentation}var {user_reg_name} = getUserReg({register});'.format(indentation=indentation,
- user_reg_name=user_reg_name,
- register=register.to_seqc())
- for register, user_reg_name in user_registers.iter_used_register_names()
- )
- self._user_registers = user_registers
-
- self._var_declarations = '{indentation}var {pos_var_name} = 0;'.format(pos_var_name=pos_var_name,
- indentation=indentation)
- self._trigger_wait_code = indentation + trigger_wait_code
- self._seqc_source = '\n'.join(self._seqc_node.to_source_code(self._waveform_manager,
- map(str, itertools.count(1)),
- line_prefix=indentation,
- pos_var_name=pos_var_name))
- self._waveform_manager.finalize()
-
- @property
- def seqc_node(self) -> 'SEQCNode':
- assert self._seqc_node is not None, "compile not called"
- return self._seqc_node
-
- @property
- def seqc_source(self) -> str:
- assert self._seqc_source is not None, "compile not called"
- return '\n'.join([self._var_declarations,
- self._user_register_source,
- self._trigger_wait_code,
- self._seqc_source])
-
- def volatile_repetition_counts(self) -> Iterable[Tuple[UserRegister, VolatileRepetitionCount]]:
- """
- Returns:
- An iterator over the register and parameter
- """
- assert self._user_registers is not None, "compile not called"
- return self._user_registers.iter_used_register_values()
-
- @property
- def name(self) -> str:
- return self._waveform_manager.program_name
-
- def parse_to_seqc(self, waveform_memory):
- raise NotImplementedError()
-
- def get_binary_waveform(self, waveform: Waveform) -> Tuple[BinaryWaveform, ...]:
- return self._waveforms[waveform]
-
- def prepare_delete(self):
- """Delete all references to this program. Cannot be used afterwards"""
- self._waveform_manager.prepare_delete()
- self._seqc_node = None
- self._seqc_source = None
-
-
-class HDAWGProgramManager:
- """This class contains everything that is needed to create the final seqc program and provides an interface to write
- the required waveforms to the file system. It does not talk to the device."""
-
- class Constants:
- PROG_SEL_REGISTER = UserRegister(zero_based_value=0)
- TRIGGER_REGISTER = UserRegister(zero_based_value=1)
- TRIGGER_RESET_MASK = bin(1 << 31)
- PROG_SEL_NONE = 0
- # if not set the register is set to PROG_SEL_NONE
- NO_RESET_MASK = bin(1 << 31)
- # set to one if playback finished
- PLAYBACK_FINISHED_MASK = bin(1 << 30)
- PROG_SEL_MASK = bin((1 << 30) - 1)
- INVERTED_PROG_SEL_MASK = bin(((1 << 32) - 1) ^ int(PROG_SEL_MASK, 2))
- IDLE_WAIT_CYCLES = 300
-
- @classmethod
- def as_dict(cls) -> Dict[str, Any]:
- return {name: value
- for name, value in vars(cls).items()
- if name[0] in string.ascii_uppercase}
-
- class GlobalVariables:
- """Global variables of the program together with their (multiline) doc string.
- The python names are uppercase."""
-
- PROG_SEL = (['Selected program index (0 -> None)'], 0)
- NEW_PROG_SEL = (('Value that gets written back to program selection register.',
- 'Used to signal that at least one program was played completely.'), 0)
- PLAYBACK_FINISHED = (('Is OR\'ed to new_prog_sel.',
- 'Set to PLAYBACK_FINISHED_MASK if a program was played completely.',), 0)
-
- @classmethod
- def as_dict(cls) -> Dict[str, Tuple[Sequence[str], int]]:
- return {name: value
- for name, value in vars(cls).items()
- if name[0] in string.ascii_uppercase}
-
- @classmethod
- def get_init_block(cls) -> str:
- lines = ['// Declare and initialize global variables']
- for var_name, (comment, initial_value) in cls.as_dict().items():
- lines.extend(f'// {comment_line}' for comment_line in comment)
- lines.append(f'var {var_name.lower()} = {initial_value};')
- lines.append('')
- return '\n'.join(lines)
-
- _PROGRAM_FUNCTION_NAME_TEMPLATE = '{program_name}_function'
- WAIT_FOR_SOFTWARE_TRIGGER = "waitForSoftwareTrigger();"
- SOFTWARE_WAIT_FOR_TRIGGER_FUNCTION_DEFINITION = (
- 'void waitForSoftwareTrigger() {\n'
- ' while (true) {\n'
- ' var trigger_register = getUserReg(TRIGGER_REGISTER);\n'
- ' if (trigger_register & TRIGGER_RESET_MASK) setUserReg(TRIGGER_REGISTER, 0);\n'
- ' if (trigger_register) return;\n'
- ' }\n'
- '}\n'
- )
- DEFAULT_COMPILER_SETTINGS = {
- 'trigger_wait_code': WAIT_FOR_SOFTWARE_TRIGGER,
- 'min_repetitions_for_for_loop': 20,
- 'min_repetitions_for_shared_wf': 1000,
- 'indentation': ' '
- }
-
- @classmethod
- def get_program_function_name(cls, program_name: str):
- if not program_name.isidentifier():
- program_name = make_valid_identifier(program_name)
- return cls._PROGRAM_FUNCTION_NAME_TEMPLATE.format(program_name=program_name)
-
- def __init__(self):
- self._waveform_memory = WaveformMemory()
- self._programs = OrderedDict() # type: MutableMapping[str, HDAWGProgramEntry]
- self._compiler_settings = [
- # default settings: None -> take cls value
- (re.compile('.*'), {'trigger_wait_code': None,
- 'min_repetitions_for_for_loop': None,
- 'min_repetitions_for_shared_wf': None,
- 'indentation': None})]
-
- def _get_compiler_settings(self, program_name: str) -> dict:
- arg_spec = inspect.getfullargspec(HDAWGProgramEntry.compile)
- required_compiler_args = (set(arg_spec.args) | set(arg_spec.kwonlyargs)) - {'self', 'available_registers'}
-
- settings = {}
- for regex, settings_dict in self._compiler_settings:
- if regex.match(program_name):
- settings.update(settings_dict)
- if required_compiler_args - set(settings):
- raise ValueError('Not all compiler arguments for program have been defined.'
- ' (the default catch all has been removed)'
- f'Missing: {required_compiler_args - set(settings)}')
- for k, v in settings.items():
- if v is None:
- settings[k] = self.DEFAULT_COMPILER_SETTINGS[k]
- return settings
-
- @property
- def waveform_memory(self):
- return self._waveform_memory
-
- def _get_low_unused_index(self):
- existing = {entry.selection_index for entry in self._programs.values()}
- for idx in itertools.count():
- if idx not in existing and idx != self.Constants.PROG_SEL_NONE:
- return idx
-
- def add_program(self, name: str, loop: Loop,
- channels: Tuple[Optional[ChannelID], ...],
- markers: Tuple[Optional[ChannelID], ...],
- amplitudes: Tuple[float, ...],
- offsets: Tuple[float, ...],
- voltage_transformations: Tuple[Optional[Callable], ...],
- sample_rate: TimeType):
- """Register the given program and translate it to seqc.
-
- TODO: Add an interface to change the trigger mode
-
- Args:
- name: Human readable name of the program (used f.i. for the function name)
- loop: The program to upload
- channels: see AWG.upload
- markers: see AWG.upload
- amplitudes: Used to sample the waveforms
- offsets: Used to sample the waveforms
- voltage_transformations: see AWG.upload
- sample_rate: Used to sample the waveforms
- """
- assert name not in self._programs
-
- selection_index = self._get_low_unused_index()
-
- # TODO: verify total number of registers
- available_registers = [UserRegister.from_seqc(idx) for idx in range(2, 16)]
-
- program_entry = HDAWGProgramEntry(loop, selection_index, self._waveform_memory, name,
- channels, markers, amplitudes, offsets, voltage_transformations, sample_rate)
-
- compiler_settings = self._get_compiler_settings(program_name=name)
-
- # TODO: put compilation in seperate function
- program_entry.compile(**compiler_settings,
- available_registers=available_registers)
-
- self._programs[name] = program_entry
-
- def get_register_values(self, name: str) -> Mapping[UserRegister, int]:
- return {register: int(parameter)
- for register, parameter in self._programs[name].volatile_repetition_counts()}
-
- def get_register_values_to_update_volatile_parameters(self, name: str,
- parameters: Mapping[str,
- numbers.Number]) -> Mapping[UserRegister,
- int]:
- """
-
- Args:
- name: Program name
- parameters: new values for volatile parameters
-
- Returns:
- A dict user_register->value that reflects the new parameter values
- """
- program_entry = self._programs[name]
- result = {}
- for register, volatile_repetition in program_entry.volatile_repetition_counts():
- new_value = volatile_repetition.update_volatile_dependencies(parameters)
- result[register] = new_value
- return result
-
- @property
- def programs(self) -> Mapping[str, HDAWGProgramEntry]:
- return MappingProxyType(self._programs)
-
- def remove(self, name: str) -> None:
- self._programs.pop(name).prepare_delete()
-
- def clear(self) -> None:
- self._waveform_memory.clear()
- self._programs.clear()
-
- def name_to_index(self, name: str) -> int:
- assert self._programs[name].name == name
- return self._programs[name].selection_index
-
- def _get_sub_program_source_code(self, program_name: str) -> str:
- program = self.programs[program_name]
- program_function_name = self.get_program_function_name(program_name)
- return "\n".join(
- [
- f"void {program_function_name}() {{",
- program.seqc_source,
- "}\n"
- ]
- )
-
- def _get_program_selection_code(self) -> str:
- return _make_program_selection_block((program.selection_index, self.get_program_function_name(program_name))
- for program_name, program in self.programs.items())
-
- def to_seqc_program(self, single_program: Optional[str] = None) -> str:
- """Generate sequencing c source code that is either capable of playing pack all uploaded programs where the
- program is selected at runtime without re-compile or always will play the same program if `single_program`
- is specified.
-
- The program selection is based on a user register in the first case.
-
- Args:
- single_program: The seqc source only contains this program if not None
-
- Returns:
- SEQC source code.
- """
- lines = []
- for const_name, const_val in self.Constants.as_dict().items():
- if isinstance(const_val, (int, str)):
- const_repr = str(const_val)
- else:
- const_repr = const_val.to_seqc()
- lines.append('const {const_name} = {const_repr};'.format(const_name=const_name, const_repr=const_repr))
-
- lines.append(self._waveform_memory.waveform_declaration())
-
- lines.append('\n// function used by manually triggered programs')
- lines.append(self.SOFTWARE_WAIT_FOR_TRIGGER_FUNCTION_DEFINITION)
-
- replacements = self._waveform_memory.waveform_name_replacements()
-
- lines.append('\n// program definitions')
- if single_program:
- lines.append(
- replace_multiple(self._get_sub_program_source_code(single_program), replacements)
- )
-
- else:
- for program_name, program in self.programs.items():
- lines.append(replace_multiple(self._get_sub_program_source_code(program_name), replacements))
-
- lines.append(self.GlobalVariables.get_init_block())
-
- lines.append('\n// runtime block')
- if single_program:
- lines.append(f"{self.get_program_function_name(single_program)}();")
- else:
- lines.append(self._get_program_selection_code())
-
- return '\n'.join(lines)
-
-
-def find_sharable_waveforms(node_cluster: Sequence['SEQCNode']) -> Optional[Sequence[bool]]:
- """Expects nodes to have a compatible stepping
-
- TODO: encode in type system?
- """
- waveform_playbacks = list(node_cluster[0].iter_waveform_playbacks())
-
- candidates = [True] * len(waveform_playbacks)
-
- for node in itertools.islice(node_cluster, 1, None):
- candidates_left = False
- for idx, (wf, node_wf) in enumerate(zip(waveform_playbacks, node.iter_waveform_playbacks())):
- if candidates[idx]:
- candidates[idx] = wf == node_wf
- candidates_left = candidates_left or candidates[idx]
-
- if not candidates_left:
- return None
-
- return candidates
-
-
-def mark_sharable_waveforms(node_cluster: Sequence['SEQCNode'], sharable_waveforms: Sequence[bool]):
- for node in node_cluster:
- for sharable, wf_playback in zip(sharable_waveforms, node.iter_waveform_playbacks()):
- if sharable:
- wf_playback.shared = True
-
-
-def _find_repetition(nodes: Deque['SEQCNode'],
- hashes: Deque[int],
- cluster_dump: List[List['SEQCNode']]) -> Tuple[
- Tuple['SEQCNode', ...],
- Tuple[int, ...],
- List['SEQCNode']
-]:
- """Finds repetitions of stepping patterns in nodes. Assumes hashes contains the stepping_hash of each node. If a
- pattern is """
- assert len(nodes) == len(hashes)
-
- max_cluster_size = len(nodes) // 2
- for cluster_size in range(max_cluster_size, 0, -1):
- n_repetitions = len(nodes) // cluster_size
- for c_idx in range(cluster_size):
- idx_a = -1 - c_idx
-
- for n in range(1, n_repetitions):
- idx_b = idx_a - n * cluster_size
- if hashes[idx_a] != hashes[idx_b] or not nodes[idx_a].same_stepping(nodes[idx_b]):
- n_repetitions = n
- break
-
- if n_repetitions < 2:
- break
-
- else:
- assert n_repetitions > 1
- # found a stepping pattern repetition of length cluster_size!
- to_dump = len(nodes) - (n_repetitions * cluster_size)
- for _ in range(to_dump):
- cluster_dump.append([nodes.popleft()])
- hashes.popleft()
-
- assert len(nodes) == n_repetitions * cluster_size
-
- if cluster_size == 1:
- current_cluster = list(nodes)
-
- cluster_template_hashes = (hashes.popleft(),)
- cluster_template: Tuple[SEQCNode] = (nodes.popleft(),)
-
- nodes.clear()
- hashes.clear()
-
- else:
- cluster_template_hashes = tuple(hashes.popleft() for _ in range(cluster_size))
- cluster_template = tuple(
- nodes.popleft() for _ in range(cluster_size)
- )
-
- current_cluster: List[SEQCNode] = [Scope(list(cluster_template))]
-
- for n in range(1, n_repetitions):
- current_cluster.append(Scope([
- nodes.popleft() for _ in range(cluster_size)
- ]))
- assert not nodes
- hashes.clear()
-
- return cluster_template, cluster_template_hashes, current_cluster
- return (), (), []
-
-
-def to_node_clusters(loop: Union[Sequence[Loop], Loop], loop_to_seqc_kwargs: dict) -> Sequence[Sequence['SEQCNode']]:
- """transform to seqc recursively noes and cluster them if they have compatible stepping"""
- assert len(loop) > 1
-
- # complexity: O( len(loop) * MAX_SUB_CLUSTER * loop.depth() )
- # I hope...
- MAX_SUB_CLUSTER = 4
-
- node_clusters: List[List[SEQCNode]] = []
-
- # this is the period that we currently are collecting
- current_period: List[SEQCNode] = []
-
- # list of already collected periods. Each period is transformed into a SEQCNode
- current_cluster: List[SEQCNode] = []
-
- # this is a template for what we are currently collecting
- current_template: Tuple[SEQCNode, ...] = ()
- current_template_hashes: Tuple[int, ...] = ()
-
- # only populated if we are looking for a node template
- last_node = loop_to_seqc(loop[0], **loop_to_seqc_kwargs)
- last_hashes = collections.deque([last_node.stepping_hash()], maxlen=MAX_SUB_CLUSTER*2)
- last_nodes = collections.deque([last_node], maxlen=MAX_SUB_CLUSTER*2)
-
- # compress all nodes in clusters of the same stepping
- for child in itertools.islice(loop, 1, None):
- current_node = loop_to_seqc(child, **loop_to_seqc_kwargs)
- current_hash = current_node.stepping_hash()
-
- if current_template:
- # we are currently collecting something
- idx = len(current_period)
- if current_template_hashes[idx] == current_hash and current_node.same_stepping(current_template[idx]):
- current_period.append(current_node)
-
- if len(current_period) == len(current_template):
- if idx == 0:
- node = current_period.pop()
- else:
- node = Scope(current_period)
- current_period = []
- current_cluster.append(node)
-
- else:
- # current template became invalid
- assert len(current_cluster) > 1
- node_clusters.append(current_cluster)
-
- assert not last_nodes
- assert not last_hashes
- last_nodes.extend(current_period)
- last_hashes.extend(current_template_hashes[:len(current_period)])
-
- current_period.clear()
-
- last_nodes.append(current_node)
- last_hashes.append(current_hash)
-
- (current_template,
- current_template_hashes,
- current_cluster) = _find_repetition(last_nodes, last_hashes,
- node_clusters)
- else:
- assert not current_period
- if len(last_nodes) == last_nodes.maxlen:
- # lookup deque is full
- node_clusters.append([last_nodes.popleft()])
- last_hashes.popleft()
-
- last_nodes.append(current_node)
- last_hashes.append(current_hash)
-
- (current_template,
- current_template_hashes,
- current_cluster) = _find_repetition(last_nodes, last_hashes,
- node_clusters)
-
- assert not (current_cluster and last_nodes)
- if current_cluster:
- node_clusters.append(current_cluster)
- node_clusters.extend([node] for node in current_period)
- node_clusters.extend([node] for node in last_nodes)
-
- return node_clusters
-
-
-def loop_to_seqc(loop: Loop,
- min_repetitions_for_for_loop: int,
- min_repetitions_for_shared_wf: int,
- waveform_to_bin: Callable[[Waveform], Tuple[BinaryWaveform, ...]],
- user_registers: UserRegisterManager) -> 'SEQCNode':
- assert min_repetitions_for_for_loop <= min_repetitions_for_shared_wf
- # At which point do we switch from indexed to shared
-
- if loop.is_leaf():
- node = WaveformPlayback(waveform_to_bin(loop.waveform))
-
- elif len(loop) == 1:
- node = loop_to_seqc(loop[0],
- min_repetitions_for_for_loop=min_repetitions_for_for_loop,
- min_repetitions_for_shared_wf=min_repetitions_for_shared_wf,
- waveform_to_bin=waveform_to_bin, user_registers=user_registers)
-
- else:
- node_clusters = to_node_clusters(loop, dict(min_repetitions_for_for_loop=min_repetitions_for_for_loop,
- min_repetitions_for_shared_wf=min_repetitions_for_shared_wf,
- waveform_to_bin=waveform_to_bin,
- user_registers=user_registers))
-
- seqc_nodes = []
-
- # identify shared waveforms in node clusters
- for node_cluster in node_clusters:
- if len(node_cluster) < min_repetitions_for_for_loop:
- seqc_nodes.extend(node_cluster)
-
- else:
- if len(node_cluster) >= min_repetitions_for_shared_wf:
- sharable_waveforms = find_sharable_waveforms(node_cluster)
- if sharable_waveforms:
- mark_sharable_waveforms(node_cluster, sharable_waveforms)
-
- seqc_nodes.append(SteppingRepeat(node_cluster))
-
- node = Scope(seqc_nodes)
-
- if loop.volatile_repetition:
- register_var = user_registers.request(loop.repetition_definition)
- return Repeat(scope=node, repetition_count=register_var)
-
- elif loop.repetition_count != 1:
- return Repeat(scope=node, repetition_count=loop.repetition_count)
- else:
- return node
-
-
-class SEQCNode(metaclass=abc.ABCMeta):
- __slots__ = ()
-
- INDENTATION = ' '
-
- @abc.abstractmethod
- def samples(self) -> int:
- pass
-
- @abc.abstractmethod
- def stepping_hash(self) -> int:
- """hash of the stepping properties of this node"""
-
- @abc.abstractmethod
- def same_stepping(self, other: 'SEQCNode'):
- pass
-
- @abc.abstractmethod
- def iter_waveform_playbacks(self) -> Iterator['WaveformPlayback']:
- pass
-
- def _get_single_indexed_playback(self) -> Optional['WaveformPlayback']:
- """Returns None if there is no or if there are more than one indexed playbacks"""
- # detect if there is only a single indexed playback
- single_indexed_playback = None
- for playback in self.iter_waveform_playbacks():
- if not playback.shared:
- if single_indexed_playback is None:
- single_indexed_playback = playback
- else:
- break
- else:
- return single_indexed_playback
- return None
-
- @abc.abstractmethod
- def _visit_nodes(self, waveform_manager: ProgramWaveformManager):
- """push all concatenated waveforms in the waveform manager"""
-
- @abc.abstractmethod
- def to_source_code(self, waveform_manager: ProgramWaveformManager, node_name_generator: Iterator[str], line_prefix: str, pos_var_name: str,
- advance_pos_var: bool = True):
- """besides creating the source code, this function registers all needed waveforms to the program manager
- 1. shared waveforms
- 2. concatenated waveforms in the correct order
-
- Args:
- waveform_manager:
- node_name_generator: generates unique names of nodes
- line_prefix:
- pos_var_name:
- advance_pos_var: Indexed playback will not advance the position if set to False. This is used internally
- to optimize repeat statements with a single indexed playback.
- Returns:
-
- """
-
- def __eq__(self, other):
- """Compare objects based on __slots__"""
- assert getattr(self, '__dict__', None) is None
- return type(self) == type(other) and all(getattr(self, attr) == getattr(other, attr)
- for base_class in inspect.getmro(type(self))
- for attr in getattr(base_class, '__slots__', ()))
-
-
-class Scope(SEQCNode):
- """Sequence of nodes"""
-
- __slots__ = ('nodes',)
-
- def __init__(self, nodes: Sequence[SEQCNode] = ()):
- self.nodes = list(nodes)
-
- def samples(self):
- return sum(node.samples() for node in self.nodes)
-
- def iter_waveform_playbacks(self) -> Iterator['WaveformPlayback']:
- for node in self.nodes:
- yield from node.iter_waveform_playbacks()
-
- def stepping_hash(self) -> int:
- return functools.reduce(int.__xor__, (node.stepping_hash() for node in self.nodes), hash(type(self)))
-
- def same_stepping(self, other: 'Scope'):
- return (type(other) is Scope and
- len(self.nodes) == len(other.nodes) and
- all(n1.same_stepping(n2) for n1, n2 in zip(self.nodes, other.nodes)))
-
- def _visit_nodes(self, waveform_manager: ProgramWaveformManager):
- for node in self.nodes:
- node._visit_nodes(waveform_manager)
-
- def to_source_code(self, waveform_manager: ProgramWaveformManager, node_name_generator: Iterator[str],
- line_prefix: str, pos_var_name: str,
- advance_pos_var: bool = True):
- for node in self.nodes:
- yield from node.to_source_code(waveform_manager,
- line_prefix=line_prefix,
- pos_var_name=pos_var_name,
- node_name_generator=node_name_generator,
- advance_pos_var=advance_pos_var)
-
- def __eq__(self, other):
- if type(other) is type(self):
- return self.nodes == other.nodes
- else:
- return NotImplemented
-
- def __repr__(self):
- return f"Scope(nodes={self.nodes!r})"
-
-
-class Repeat(SEQCNode):
- """"""
- __slots__ = ('repetition_count', 'scope')
- INITIAL_POSITION_NAME_TEMPLATE = 'init_pos_{node_name}'
- FOR_LOOP_NAME_TEMPLATE = 'idx_{node_name}'
-
- class _AdvanceStrategy:
- """describes what happens how this node interacts with the position variable"""
- INITIAL_RESET = 'initial_reset'
- POST_ADVANCE = 'post_advance'
- IGNORE = 'ignore'
-
- def __init__(self, repetition_count: Union[int, str], scope: SEQCNode):
- """
- Args:
- repetition_count: A const integer value or a string that is expected to be a "var"
- scope: The repeated scope
- """
- if isinstance(repetition_count, int):
- assert repetition_count > 1
- else:
- assert isinstance(repetition_count, str) and repetition_count.isidentifier()
-
- self.repetition_count = repetition_count
- self.scope = scope
-
- def samples(self):
- return self.scope.samples()
-
- def same_stepping(self, other: 'Repeat'):
- return (type(self) == type(other) and
- self.repetition_count == other.repetition_count and
- self.scope.same_stepping(other.scope))
-
- def stepping_hash(self) -> int:
- return hash((type(self), self.repetition_count, self.scope.stepping_hash()))
-
- def iter_waveform_playbacks(self) -> Iterator['WaveformPlayback']:
- return self.scope.iter_waveform_playbacks()
-
- def _visit_nodes(self, waveform_manager: ProgramWaveformManager):
- self.scope._visit_nodes(waveform_manager)
-
- def _get_position_advance_strategy(self):
- """Deduct the optimal position advance strategy:
-
- There is more than one indexed playback -> position needs to be advanced during each iteration and set back to
- initial value at the begin of each new iteration
- There is exactly one indexed playback -> The position is not advanced in the body but needs to be advanced after
- all repetitions are done
- There is no indexed playback -> We do not care about the position at all
- """
- self_samples = self.samples()
- if self_samples > 0:
- single_playback = self.scope._get_single_indexed_playback()
- if single_playback is None or single_playback.samples() != self_samples:
- # TODO: I am not sure whether the 'single_playback.samples() != self_samples' is necessary
- # there is more than one indexed playback
- return self._AdvanceStrategy.INITIAL_RESET
- else:
- # there is only a single indexed playback
- return self._AdvanceStrategy.POST_ADVANCE
- else:
- # there is no indexed playback
- return self._AdvanceStrategy.IGNORE
-
- def to_source_code(self, waveform_manager: ProgramWaveformManager, node_name_generator: Iterator[str],
- line_prefix: str, pos_var_name: str, advance_pos_var: bool = True):
- body_prefix = line_prefix + self.INDENTATION
-
- advance_strategy = self._get_position_advance_strategy() if advance_pos_var else self._AdvanceStrategy.IGNORE
- inner_advance_pos_var = advance_strategy == self._AdvanceStrategy.INITIAL_RESET
-
- def get_node_name():
- """Helper to assert node name only generated when needed and only generated once"""
- if getattr(get_node_name, 'node_name', None) is None:
- get_node_name.node_name = next(node_name_generator)
- return get_node_name.node_name
-
- if advance_strategy == self._AdvanceStrategy.INITIAL_RESET:
- initial_position_name = self.INITIAL_POSITION_NAME_TEMPLATE.format(node_name=get_node_name())
-
- # store initial position
- yield '{line_prefix}var {init_pos_name} = {pos_var_name};'.format(line_prefix=line_prefix,
- init_pos_name=initial_position_name,
- pos_var_name=pos_var_name)
-
- if isinstance(self.repetition_count, int):
- yield '{line_prefix}repeat({repetition_count}) {{'.format(line_prefix=line_prefix,
- repetition_count=self.repetition_count)
- else:
- # repeat requires a const-expression so we need to use a for loop for user reg vars
- assert isinstance(self.repetition_count, str)
- loop_var = self.FOR_LOOP_NAME_TEMPLATE.format(node_name=get_node_name())
- yield '{line_prefix}var {loop_var};'.format(line_prefix=line_prefix, loop_var=loop_var)
- yield ('{line_prefix}for({loop_var} = 0; '
- '{loop_var} < {repetition_count}; '
- '{loop_var} = {loop_var} + 1) {{').format(line_prefix=line_prefix,
- loop_var=loop_var,
- repetition_count=self.repetition_count)
-
- if advance_strategy == self._AdvanceStrategy.INITIAL_RESET:
- yield ('{body_prefix}{pos_var_name} = {init_pos_name};'
- '').format(body_prefix=body_prefix,
- pos_var_name=pos_var_name,
- init_pos_name=initial_position_name)
- yield from self.scope.to_source_code(waveform_manager,
- line_prefix=body_prefix, pos_var_name=pos_var_name,
- node_name_generator=node_name_generator,
- advance_pos_var=inner_advance_pos_var)
- yield '{line_prefix}}}'.format(line_prefix=line_prefix)
-
- if advance_strategy == self._AdvanceStrategy.POST_ADVANCE:
- yield '{line_prefix}{pos_var_name} = {pos_var_name} + {samples};'.format(line_prefix=line_prefix,
- pos_var_name=pos_var_name,
- samples=self.samples())
-
-
-class SteppingRepeat(SEQCNode):
- STEPPING_REPEAT_COMMENT = ' // stepping repeat'
- __slots__ = ('node_cluster',)
-
- def __init__(self, node_cluster: Sequence[SEQCNode]):
- self.node_cluster = node_cluster
-
- def samples(self) -> int:
- return self.repetition_count * self.node_cluster[0].samples()
-
- @property
- def repetition_count(self):
- return len(self.node_cluster)
-
- def iter_waveform_playbacks(self) -> Iterator['WaveformPlayback']:
- for node in self.node_cluster:
- yield from node.iter_waveform_playbacks()
-
- def stepping_hash(self) -> int:
- return hash((type(self), self.node_cluster[0].stepping_hash()))
-
- def same_stepping(self, other: 'SteppingRepeat'):
- return (type(other) is SteppingRepeat and
- len(self.node_cluster) == len(other.node_cluster) and
- self.node_cluster[0].same_stepping(other.node_cluster[0]))
-
- def _visit_nodes(self, waveform_manager: ProgramWaveformManager):
- for node in self.node_cluster:
- node._visit_nodes(waveform_manager)
-
- def to_source_code(self, waveform_manager: ProgramWaveformManager, node_name_generator: Iterator[str],
- line_prefix: str, pos_var_name: str,
- advance_pos_var: bool = True):
- body_prefix = line_prefix + self.INDENTATION
- repeat_open = '{line_prefix}repeat({repetition_count}) {{' + self.STEPPING_REPEAT_COMMENT
- yield repeat_open.format(line_prefix=line_prefix,
- repetition_count=self.repetition_count)
- yield from self.node_cluster[0].to_source_code(waveform_manager,
- line_prefix=body_prefix, pos_var_name=pos_var_name,
- node_name_generator=node_name_generator,
- advance_pos_var=advance_pos_var)
-
- # register remaining concatenated waveforms
- for node in itertools.islice(self.node_cluster, 1, None):
- node._visit_nodes(waveform_manager)
-
- yield '{line_prefix}}}'.format(line_prefix=line_prefix)
-
-
-class WaveformPlayback(SEQCNode):
- ADVANCE_DISABLED_COMMENT = ' // advance disabled do to parent repetition'
- ENABLE_DYNAMIC_RATE_REDUCTION = False
-
- __slots__ = ('waveform', 'shared', 'rate')
-
- def __init__(self, waveform: Tuple[BinaryWaveform, ...], shared: bool = False, rate: int = None):
- assert isinstance(waveform, tuple)
- if self.ENABLE_DYNAMIC_RATE_REDUCTION and rate is None:
- for wf in waveform:
- rate = wf.dynamic_rate(12 if rate is None else rate)
- self.waveform = waveform
- self.shared = shared
- self.rate = rate
-
- def __repr__(self):
- return f"WaveformPlayback(<{id(self)}>)"
-
- def samples(self) -> int:
- """Samples consumed in the big concatenated waveform"""
- if self.shared:
- return 0
- else:
- wf_lens = set(map(len, self.waveform))
- assert len(wf_lens) == 1
- wf_len, = wf_lens
- if self.rate is not None:
- wf_len //= (1 << self.rate)
- return wf_len
-
- def rate_reduced_waveform(self) -> Tuple[BinaryWaveform]:
- if self.rate is None:
- return self.waveform
- else:
- return tuple(BinaryWaveform(wf.data.reshape((-1, 3))[::(1 << self.rate), :].ravel())
- for wf in self.waveform)
-
- def stepping_hash(self) -> int:
- if self.shared:
- return hash((type(self), self.waveform))
- else:
- return hash((type(self), self.samples()))
-
- def same_stepping(self, other: 'WaveformPlayback') -> bool:
- same_type = type(self) is type(other) and self.shared == other.shared
- if self.shared:
- return same_type and self.rate == other.rate and self.waveform == other.waveform
- else:
- return same_type and self.samples() == other.samples()
-
- def iter_waveform_playbacks(self) -> Iterator['WaveformPlayback']:
- yield self
-
- def _visit_nodes(self, waveform_manager: ProgramWaveformManager):
- if not self.shared:
- waveform_manager.request_concatenated(self.rate_reduced_waveform())
-
- def to_source_code(self, waveform_manager: ProgramWaveformManager,
- node_name_generator: Iterator[str], line_prefix: str, pos_var_name: str,
- advance_pos_var: bool = True):
- rate_adjustment = "" if self.rate is None else f", {self.rate}"
- if self.shared:
- yield f'{line_prefix}playWave(' \
- f'{waveform_manager.request_shared(self.rate_reduced_waveform())}' \
- f'{rate_adjustment});'
- else:
- wf_name = waveform_manager.request_concatenated(self.rate_reduced_waveform())
- wf_len = self.samples()
- play_cmd = f'{line_prefix}playWaveIndexed({wf_name}, {pos_var_name}, {wf_len}{rate_adjustment});'
-
- if advance_pos_var:
- advance_cmd = f' {pos_var_name} = {pos_var_name} + {wf_len};'
- else:
- advance_cmd = self.ADVANCE_DISABLED_COMMENT
- yield play_cmd + advance_cmd
-
-
-_PROGRAM_SELECTION_BLOCK = """\
-while (true) {{
- // read program selection value
- prog_sel = getUserReg(PROG_SEL_REGISTER);
-
- // calculate value to write back to PROG_SEL_REGISTER
- new_prog_sel = prog_sel | playback_finished;
- if (!(prog_sel & NO_RESET_MASK)) new_prog_sel &= INVERTED_PROG_SEL_MASK;
- setUserReg(PROG_SEL_REGISTER, new_prog_sel);
-
- // reset playback flag
- playback_finished = 0;
-
- // only use part of prog sel that does not mean other things to select the program.
- prog_sel &= PROG_SEL_MASK;
-
- switch (prog_sel) {{
-{program_cases}
- default:
- wait(IDLE_WAIT_CYCLES);
- }}
-}}"""
-
-_PROGRAM_SELECTION_CASE = """\
- case {selection_index}:
- {program_function_name}();
- waitWave();
- playback_finished = PLAYBACK_FINISHED_MASK;"""
-
-
-def _make_program_selection_block(programs: Iterable[Tuple[int, str]]):
- program_cases = []
- for selection_index, program_function_name in programs:
- program_cases.append(_PROGRAM_SELECTION_CASE.format(selection_index=selection_index,
- program_function_name=program_function_name))
- return _PROGRAM_SELECTION_BLOCK.format(program_cases="\n".join(program_cases))
diff --git a/qupulse/_program/tabor.py b/qupulse/_program/tabor.py
index 64002d969..403f0d35d 100644
--- a/qupulse/_program/tabor.py
+++ b/qupulse/_program/tabor.py
@@ -1,5 +1,11 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+import dataclasses
import sys
-from typing import NamedTuple, Optional, List, Generator, Tuple, Sequence, Mapping, Union, Dict, FrozenSet, cast
+from typing import NamedTuple, Optional, List, Generator, Tuple, Sequence, Mapping, Union, Dict, FrozenSet, cast,\
+ Hashable
from enum import Enum
import operator
from collections import OrderedDict
@@ -10,10 +16,10 @@
from qupulse.utils.types import ChannelID, TimeType
from qupulse.hardware.awgs.base import ProgramEntry
-from qupulse.hardware.util import get_sample_times, voltage_to_uint16
-from qupulse._program.waveforms import Waveform
-from qupulse._program._loop import Loop
-from qupulse._program.volatile import VolatileRepetitionCount, VolatileProperty
+from qupulse.hardware.util import get_sample_times, voltage_to_uint16, find_positions
+from qupulse.program.waveforms import Waveform
+from qupulse.program.loop import Loop
+from qupulse.program.volatile import VolatileRepetitionCount, VolatileProperty
assert(sys.byteorder == 'little')
@@ -406,7 +412,7 @@ def __init__(self,
else:
mode = TaborSequencing.SINGLE
- super().__init__(loop=program,
+ super().__init__(program=program,
channels=channels,
markers=markers,
amplitudes=amplitudes,
@@ -428,7 +434,7 @@ def __init__(self,
else:
self.setup_advanced_sequence_mode()
- self._sampled_segments = self._calc_sampled_segments()
+ self._sampled_segments, self._waveform_to_segment = self._calc_sampled_segments()
@property
def markers(self) -> Tuple[Optional[ChannelID], Optional[ChannelID]]:
@@ -462,19 +468,22 @@ def _marker_data(self, waveform: Waveform, time: np.ndarray, marker_idx: int):
marker = self._markers[marker_idx]
return waveform.get_sampled(channel=marker, sample_times=time) != 0
- def _calc_sampled_segments(self) -> Tuple[Sequence[TaborSegment], Sequence[int]]:
+ def _calc_sampled_segments(self) -> Tuple[Tuple[Sequence[TaborSegment], Sequence[int]], Sequence[int]]:
"""
Returns:
- (segments, segment_lengths)
+ ((segments, segment_lengths), waveform_to_segment)
"""
- time_array, segment_lengths = get_sample_times(self._parsed_program.waveforms, self._sample_rate)
+ time_array, waveform_samples = get_sample_times(self._parsed_program.waveforms, self._sample_rate)
- if np.any(segment_lengths % 16 > 0) or np.any(segment_lengths < 192):
+ if np.any(waveform_samples % 16 > 0) or np.any(waveform_samples < 192):
raise TaborException('At least one waveform has a length that is smaller 192 or not a multiple of 16')
- segments = []
- for i, waveform in enumerate(self._parsed_program.waveforms):
- t = time_array[:segment_lengths[i]]
+ segments: Dict[TaborSegment, int] = {}
+ segment_lengths = []
+
+ waveform_to_segment = []
+ for waveform, n_samples in zip(self._parsed_program.waveforms, waveform_samples):
+ t = time_array[:n_samples]
marker_time = t[::2]
segment_a = self._channel_data(waveform, t, 0)
segment_b = self._channel_data(waveform, t, 1)
@@ -486,8 +495,12 @@ def _calc_sampled_segments(self) -> Tuple[Sequence[TaborSegment], Sequence[int]]
ch_b=segment_b,
marker_a=marker_a,
marker_b=marker_b)
- segments.append(segment)
- return segments, segment_lengths
+ previous_segment_count = len(segments)
+ segment_idx = segments.setdefault(segment, previous_segment_count)
+ waveform_to_segment.append(segment_idx)
+ if segment_idx == previous_segment_count:
+ segment_lengths.append(n_samples)
+ return (list(segments.keys()), np.array(segment_lengths, dtype=np.uint64)), waveform_to_segment
def setup_single_sequence_mode(self) -> None:
assert self.program.depth() == 1
@@ -554,10 +567,13 @@ def update_volatile_parameters(self, parameters: Mapping[str, numbers.Number]) -
return modifications
- def get_sequencer_tables(self): # -> List[List[TableDescription, Optional[MappedParameter]]]:
- return self._parsed_program.sequencer_tables
+ def get_sequencer_tables(self) -> Sequence[Sequence[Tuple[TableDescription, Optional[VolatileProperty]]]]:
+ wf_to_seq = self._waveform_to_segment
+ return [[(TableDescription(rep_count, wf_to_seq[elem_idx], jump), volatile)
+ for (rep_count, elem_idx, jump), volatile in sequencer_table]
+ for sequencer_table in self._parsed_program.sequencer_tables]
- def get_advanced_sequencer_table(self) -> List[TableEntry]:
+ def get_advanced_sequencer_table(self) -> Sequence[TableEntry]:
"""Advanced sequencer table that can be used via the download_adv_seq_table pytabor command"""
return self._parsed_program.advanced_sequencer_table
@@ -644,12 +660,12 @@ def prepare_program_for_advanced_sequence_mode(program: Loop, min_seq_len: int,
i += 1
-ParsedProgram = NamedTuple('ParsedProgram', [('advanced_sequencer_table', Sequence[TableEntry]),
- ('sequencer_tables', Sequence[Sequence[
- Tuple[TableDescription, Optional[VolatileProperty]]]]),
- ('waveforms', Tuple[Waveform, ...]),
- ('volatile_parameter_positions', Dict[Union[int, Tuple[int, int]],
- VolatileRepetitionCount])])
+@dataclasses.dataclass
+class ParsedProgram:
+ advanced_sequencer_table: Sequence[TableEntry]
+ sequencer_tables: Sequence[Sequence[Tuple[TableDescription, Optional[VolatileProperty]]]]
+ waveforms: Tuple[Waveform, ...]
+ volatile_parameter_positions: Dict[Union[int, Tuple[int, int]], VolatileRepetitionCount]
def parse_aseq_program(program: Loop, used_channels: FrozenSet[ChannelID]) -> ParsedProgram:
@@ -726,3 +742,91 @@ def parse_single_seq_program(program: Loop, used_channels: FrozenSet[ChannelID])
waveforms=tuple(waveforms.keys()),
volatile_parameter_positions=volatile_parameter_positions
)
+
+
+def find_place_for_segments_in_memory(
+ current_segment_hashes: np.ndarray,
+ current_segment_references: np.ndarray,
+ current_segment_capacities: np.ndarray,
+ total_capacity: int,
+ new_segment_hashes: Sequence[int],
+ new_segment_lengths: Sequence[int]) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
+ """
+ 1. Find known segments
+ 2. Find empty spaces with fitting length
+ 3. Find empty spaces with bigger length
+ 4. Amend remaining segments
+ :param segments:
+ :param segment_lengths:
+ :return:
+ """
+ new_segment_hashes = np.asarray(new_segment_hashes)
+ new_segment_lengths = np.asarray(new_segment_lengths)
+
+ waveform_to_segment = find_positions(current_segment_hashes, new_segment_hashes)
+
+ # separate into known and unknown
+ unknown = waveform_to_segment == -1
+ known = ~unknown
+
+ known_pos_in_memory = waveform_to_segment[known]
+
+ assert len(known_pos_in_memory) == 0 or np.all(current_segment_hashes[known_pos_in_memory] == new_segment_hashes[known])
+
+ new_reference_counter = current_segment_references.copy()
+ new_reference_counter[known_pos_in_memory] += 1
+
+ to_upload_size = np.sum(new_segment_lengths[unknown] + 16)
+ free_points_in_total = total_capacity - np.sum(current_segment_capacities[current_segment_references > 0])
+ if free_points_in_total < to_upload_size:
+ raise RuntimeError(f'Not enough free memory. Required {to_upload_size}. Available: {free_points_in_total}')
+
+ to_amend = cast(np.ndarray, unknown)
+ to_insert = np.full(len(new_segment_hashes), fill_value=-1, dtype=np.int64)
+
+ reserved_indices = np.flatnonzero(new_reference_counter > 0)
+ first_free = reserved_indices[-1] + 1 if len(reserved_indices) else 0
+
+ free_segments = new_reference_counter[:first_free] == 0
+ free_segment_count = np.sum(free_segments)
+
+ # look for a free segment place with the same length
+ for segment_idx in np.flatnonzero(to_amend):
+ if free_segment_count == 0:
+ break
+
+ pos_of_same_length = np.logical_and(free_segments,
+ new_segment_lengths[segment_idx] == current_segment_capacities[:first_free])
+ idx_same_length = np.argmax(pos_of_same_length)
+ if pos_of_same_length[idx_same_length]:
+ free_segments[idx_same_length] = False
+ free_segment_count -= 1
+
+ to_amend[segment_idx] = False
+ to_insert[segment_idx] = idx_same_length
+
+ # try to find places that are larger than the segments to fit in starting with the large segments and large
+ # free spaces
+ segment_indices = np.flatnonzero(to_amend)[np.argsort(new_segment_lengths[to_amend], kind='stable')[::-1]]
+ capacities = current_segment_capacities[:first_free]
+ for segment_idx in segment_indices:
+ free_capacities = capacities[free_segments]
+ free_segments_indices = np.flatnonzero(free_segments)[np.argsort(free_capacities, kind='stable')[::-1]]
+
+ if len(free_segments_indices) == 0:
+ break
+
+ fitting_segment = np.argmax((free_capacities >= new_segment_lengths[segment_idx])[::-1])
+ fitting_segment = free_segments_indices[fitting_segment]
+ if current_segment_capacities[fitting_segment] >= new_segment_lengths[segment_idx]:
+ free_segments[fitting_segment] = False
+ to_amend[segment_idx] = False
+ to_insert[segment_idx] = fitting_segment
+
+ free_points_at_end = total_capacity - np.sum(current_segment_capacities[:first_free])
+ if np.sum(new_segment_lengths[to_amend] + 16) > free_points_at_end:
+ raise RuntimeError('Fragmentation does not allow upload.',
+ np.sum(new_segment_lengths[to_amend] + 16),
+ free_points_at_end)
+
+ return waveform_to_segment, to_amend, to_insert
diff --git a/qupulse/_program/transformation.py b/qupulse/_program/transformation.py
index 66a1641f9..f82342816 100644
--- a/qupulse/_program/transformation.py
+++ b/qupulse/_program/transformation.py
@@ -1,400 +1,11 @@
-from typing import Any, Mapping, Set, Tuple, Sequence, AbstractSet, Union, TYPE_CHECKING, Hashable
-from abc import abstractmethod
-from numbers import Real
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
-import numpy as np
+from qupulse.program.transformation import *
-from qupulse import ChannelID
-from qupulse.comparable import Comparable
-from qupulse.utils.types import SingletonABCMeta, frozendict
-from qupulse.expressions import ExpressionScalar
+import qupulse.program.transformation
+__all__ = qupulse.program.transformation.__all__
-_TrafoValue = Union[Real, ExpressionScalar]
-
-
-class Transformation(Comparable):
- _identity_singleton = None
- """Transforms numeric time-voltage values for multiple channels to other time-voltage values. The number and names
- of input and output channels might differ."""
-
- @abstractmethod
- def __call__(self, time: Union[np.ndarray, float],
- data: Mapping[ChannelID, Union[np.ndarray, float]]) -> Mapping[ChannelID, Union[np.ndarray, float]]:
- """Apply transformation to data
- Args:
- time:
- data:
-
- Returns:
- transformed: A DataFrame that has been transformed with index == output_channels
- """
-
- @abstractmethod
- def get_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
- """Return the channel identifiers"""
-
- @abstractmethod
- def get_input_channels(self, output_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
- """Channels that are required for getting data for the requested output channel"""
-
- def chain(self, next_transformation: 'Transformation') -> 'Transformation':
- if next_transformation is IdentityTransformation():
- return self
- else:
- return chain_transformations(self, next_transformation)
-
- def is_constant_invariant(self):
- """Signals if the transformation always maps constants to constants."""
- return False
-
- def get_constant_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
- return frozenset()
-
-
-class IdentityTransformation(Transformation, metaclass=SingletonABCMeta):
- def __call__(self, time: Union[np.ndarray, float],
- data: Mapping[ChannelID, Union[np.ndarray, float]]) -> Mapping[ChannelID, Union[np.ndarray, float]]:
- return data
-
- def get_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
- return input_channels
-
- @property
- def compare_key(self) -> None:
- return None
-
- def get_input_channels(self, output_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
- return output_channels
-
- def chain(self, next_transformation: Transformation) -> Transformation:
- return next_transformation
-
- def __repr__(self):
- return 'IdentityTransformation()'
-
- def is_constant_invariant(self):
- """Signals if the transformation always maps constants to constants."""
- return True
-
- def get_constant_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
- return input_channels
-
-
-class ChainedTransformation(Transformation):
- def __init__(self, *transformations: Transformation):
- self._transformations = transformations
-
- @property
- def transformations(self) -> Tuple[Transformation, ...]:
- return self._transformations
-
- def get_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
- for transformation in self._transformations:
- input_channels = transformation.get_output_channels(input_channels)
- return input_channels
-
- def get_input_channels(self, output_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
- for transformation in reversed(self._transformations):
- output_channels = transformation.get_input_channels(output_channels)
- return output_channels
-
- def __call__(self, time: Union[np.ndarray, float],
- data: Mapping[ChannelID, Union[np.ndarray, float]]) -> Mapping[ChannelID, Union[np.ndarray, float]]:
- for transformation in self._transformations:
- data = transformation(time, data)
- return data
-
- @property
- def compare_key(self) -> Tuple[Transformation, ...]:
- return self._transformations
-
- def chain(self, next_transformation) -> Transformation:
- return chain_transformations(*self.transformations, next_transformation)
-
- def __repr__(self):
- return f'{type(self).__name__}{self._transformations!r}'
-
- def is_constant_invariant(self):
- """Signals if the transformation always maps constants to constants."""
- return all(trafo.is_constant_invariant() for trafo in self._transformations)
-
- def get_constant_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
- for trafo in self._transformations:
- input_channels = trafo.get_constant_output_channels(input_channels)
- return input_channels
-
-
-class LinearTransformation(Transformation):
- def __init__(self,
- transformation_matrix: np.ndarray,
- input_channels: Sequence[ChannelID],
- output_channels: Sequence[ChannelID]):
- """
-
- Args:
- transformation_matrix: Matrix describing the transformation with shape (output_channels, input_channels)
- input_channels: Channel ids of the columns
- output_channels: Channel ids of the rows
- """
- transformation_matrix = np.asarray(transformation_matrix)
-
- if transformation_matrix.shape != (len(output_channels), len(input_channels)):
- raise ValueError('Shape of transformation matrix does not match to the given channels')
-
- output_sorter = np.argsort(output_channels)
- transformation_matrix = transformation_matrix[output_sorter, :]
-
- input_sorter = np.argsort(input_channels)
- transformation_matrix = transformation_matrix[:, input_sorter]
-
- self._matrix = transformation_matrix
- self._input_channels = tuple(sorted(input_channels))
- self._output_channels = tuple(sorted(output_channels))
- self._input_channels_set = frozenset(self._input_channels)
- self._output_channels_set = frozenset(self._output_channels)
-
- def __call__(self, time: Union[np.ndarray, float],
- data: Mapping[ChannelID, Union[np.ndarray, float]]) -> Mapping[ChannelID, Union[np.ndarray, float]]:
- data_out = {forwarded_channel: data[forwarded_channel]
- for forwarded_channel in set(data.keys()).difference(self._input_channels)}
-
- if len(data_out) == len(data):
- # only forwarded data
- return data_out
-
- try:
- data_in = np.stack([data[in_channel] for in_channel in self._input_channels])
- except KeyError as error:
- raise KeyError('Invalid input channels', set(data.keys()), set(self._input_channels)) from error
-
- transformed_data = self._matrix @ data_in
-
- for idx, out_channel in enumerate(self._output_channels):
- data_out[out_channel] = transformed_data[idx, ...]
-
- return data_out
-
- def get_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
- if not input_channels >= self._input_channels_set:
- # input_channels is not a superset of the required input channels
- raise KeyError('Invalid input channels', input_channels, self._input_channels_set)
-
- return (input_channels - self._input_channels_set) | self._output_channels_set
-
- def get_input_channels(self, output_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
- forwarded = output_channels - self._output_channels_set
- if not forwarded.isdisjoint(self._input_channels):
- raise KeyError('Is input channel', forwarded & self._input_channels_set)
- elif output_channels.isdisjoint(self._output_channels):
- return output_channels
- else:
- return forwarded | self._input_channels_set
-
- @property
- def compare_key(self) -> Tuple[Tuple[ChannelID], Tuple[ChannelID], bytes]:
- return self._input_channels, self._output_channels, self._matrix.tobytes()
-
- def __repr__(self):
- return ('LinearTransformation('
- 'transformation_matrix={transformation_matrix},'
- 'input_channels={input_channels},'
- 'output_channels={output_channels})').format(transformation_matrix=self._matrix.tolist(),
- input_channels=self._input_channels,
- output_channels=self._output_channels)
-
- def is_constant_invariant(self):
- """Signals if the transformation always maps constants to constants."""
- return True
-
- def get_constant_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
- return input_channels
-
-
-class OffsetTransformation(Transformation):
- def __init__(self, offsets: Mapping[ChannelID, _TrafoValue]):
- """Adds an offset to each channel specified in offsets.
-
- Channels not in offsets are forewarded
-
- Args:
- offsets: Channel -> offset mapping
- """
- self._offsets = frozendict(offsets)
- assert _are_valid_transformation_expressions(self._offsets), f"Not valid transformation expressions: {self._offsets}"
-
- def __call__(self, time: Union[np.ndarray, float],
- data: Mapping[ChannelID, Union[np.ndarray, float]]) -> Mapping[ChannelID, Union[np.ndarray, float]]:
- offsets = _instantiate_expression_dict(time, self._offsets)
- return {channel: channel_values + offsets[channel] if channel in offsets else channel_values
- for channel, channel_values in data.items()}
-
- def get_input_channels(self, output_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
- return output_channels
-
- def get_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
- return input_channels
-
- @property
- def compare_key(self) -> Hashable:
- return self._offsets
-
- def __repr__(self):
- return f'{type(self).__name__}({dict(self._offsets)!r})'
-
- def is_constant_invariant(self):
- """Signals if the transformation always maps constants to constants."""
- return not _has_time_dependent_values(self._offsets)
-
- def get_constant_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
- return _get_constant_output_channels(self._offsets, input_channels)
-
-
-class ScalingTransformation(Transformation):
- def __init__(self, factors: Mapping[ChannelID, _TrafoValue]):
- self._factors = frozendict(factors)
- assert _are_valid_transformation_expressions(self._factors), f"Not valid transformation expressions: {self._factors}"
-
- def __call__(self, time: Union[np.ndarray, float],
- data: Mapping[ChannelID, Union[np.ndarray, float]]) -> Mapping[ChannelID, Union[np.ndarray, float]]:
- factors = _instantiate_expression_dict(time, self._factors)
- return {channel: channel_values * factors[channel] if channel in factors else channel_values
- for channel, channel_values in data.items()}
-
- def get_input_channels(self, output_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
- return output_channels
-
- def get_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
- return input_channels
-
- @property
- def compare_key(self) -> Hashable:
- return self._factors
-
- def __repr__(self):
- return f'{type(self).__name__}({dict(self._factors)!r})'
-
- def is_constant_invariant(self):
- """Signals if the transformation always maps constants to constants."""
- return not _has_time_dependent_values(self._factors)
-
- def get_constant_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
- return _get_constant_output_channels(self._factors, input_channels)
-
-
-try:
- if TYPE_CHECKING:
- import pandas
- PandasDataFrameType = pandas.DataFrame
- else:
- PandasDataFrameType = Any
-
- def linear_transformation_from_pandas(transformation: PandasDataFrameType) -> LinearTransformation:
- """ Creates a LinearTransformation object out of a pandas data frame.
-
- Args:
- transformation (pandas.DataFrame): The pandas.DataFrame object out of which a LinearTransformation will be formed.
-
- Returns:
- the created LinearTransformation instance
- """
- return LinearTransformation(transformation.values, transformation.columns, transformation.index)
-
- LinearTransformation.from_pandas = linear_transformation_from_pandas
-except ImportError:
- pass
-
-
-class ParallelChannelTransformation(Transformation):
- def __init__(self, channels: Mapping[ChannelID, _TrafoValue]):
- """Set channel values to given values regardless their former existence. The values can be time dependent
- expressions.
-
- Args:
- channels: Channels present in this map are set to the given value.
- """
- self._channels: Mapping[ChannelID, _TrafoValue] = frozendict(channels.items())
- assert _are_valid_transformation_expressions(self._channels), f"Not valid transformation expressions: {self._channels}"
-
- def __call__(self, time: Union[np.ndarray, float],
- data: Mapping[ChannelID, Union[np.ndarray, float]]) -> Mapping[ChannelID, Union[np.ndarray, float]]:
- overwritten = self._instantiated_values(time)
- return {**data, **overwritten}
-
- def _instantiated_values(self, time):
- scope = {'t': time}
- return {channel: value.evaluate_in_scope(scope) if hasattr(value, 'evaluate_in_scope') else np.full_like(time, fill_value=value, dtype=float)
- for channel, value in self._channels.items()}
-
- @property
- def compare_key(self) -> Hashable:
- return self._channels
-
- def get_input_channels(self, output_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
- return output_channels - self._channels.keys()
-
- def get_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
- return input_channels | self._channels.keys()
-
- def __repr__(self):
- return f'{type(self).__name__}({dict(self._channels)!r})'
-
- def is_constant_invariant(self):
- """Signals if the transformation always maps constants to constants."""
- return not _has_time_dependent_values(self._channels)
-
- def get_constant_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
- output_channels = set(input_channels)
- for ch, value in self._channels.items():
- if hasattr(value, 'variables'):
- output_channels.discard(ch)
- else:
- output_channels.add(ch)
-
- return output_channels
-
-
-def chain_transformations(*transformations: Transformation) -> Transformation:
- parsed_transformations = []
- for transformation in transformations:
- if transformation is IdentityTransformation() or transformation is None:
- pass
- elif isinstance(transformation, ChainedTransformation):
- parsed_transformations.extend(transformation.transformations)
- else:
- parsed_transformations.append(transformation)
- if len(parsed_transformations) == 0:
- return IdentityTransformation()
- elif len(parsed_transformations) == 1:
- return parsed_transformations[0]
- else:
- return ChainedTransformation(*parsed_transformations)
-
-
-def _instantiate_expression_dict(time, expressions: Mapping[str, _TrafoValue]) -> Mapping[str, Union[Real, np.ndarray]]:
- scope = {'t': time}
- modified_expressions = {}
- for name, value in expressions.items():
- if hasattr(value, 'evaluate_in_scope'):
- modified_expressions[name] = value.evaluate_in_scope(scope)
- if modified_expressions:
- return {**expressions, **modified_expressions}
- else:
- return expressions
-
-
-def _has_time_dependent_values(expressions: Mapping[ChannelID, _TrafoValue]) -> bool:
- return any(hasattr(value, 'variables')
- for value in expressions.values())
-
-
-def _get_constant_output_channels(expressions: Mapping[ChannelID, _TrafoValue],
- constant_input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
- return {ch
- for ch in constant_input_channels
- if not hasattr(expressions.get(ch, None), 'variables')}
-
-def _are_valid_transformation_expressions(expressions: Mapping[ChannelID, _TrafoValue]) -> bool:
- return all(expr.variables == ('t',)
- for expr in expressions.values()
- if hasattr(expr, 'variables'))
+del qupulse
diff --git a/qupulse/_program/volatile.py b/qupulse/_program/volatile.py
index ab76e8ecc..3c2f28518 100644
--- a/qupulse/_program/volatile.py
+++ b/qupulse/_program/volatile.py
@@ -1,70 +1,11 @@
-from typing import NamedTuple, Mapping
-import warnings
-import numbers
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+from qupulse.program.volatile import *
-from qupulse.parameter_scope import Scope, MappedScope, JointScope
-from qupulse.expressions import Expression, ExpressionScalar
-from qupulse.utils.types import FrozenDict, FrozenMapping
-from qupulse.utils import is_integer
+import qupulse.program.volatile
+__all__ = qupulse.program.volatile.__all__
-VolatileProperty = NamedTuple('VolatileProperty', [('expression', Expression),
- ('dependencies', FrozenMapping[str, Expression])])
-VolatileProperty.__doc__ = """Hashable representation of a volatile program property. It does not contain the concrete
-value. Using the dependencies attribute to calculate the value might yield unexpected results."""
-
-
-class VolatileValue:
- """Not hashable"""
-
- def __init__(self, expression: ExpressionScalar, scope: Scope):
- self._expression = expression
- self._scope = scope
-
- @property
- def volatile_property(self) -> VolatileProperty:
- dependencies = self._scope.get_volatile_parameters()
- dependencies = FrozenDict({parameter_name: dependencies[parameter_name]
- for parameter_name in self._expression.variables
- if parameter_name in dependencies})
- return VolatileProperty(expression=self._expression, dependencies=dependencies)
-
- @classmethod
- def operation(cls, expression, **operands):
- expression = Expression(expression)
- assert set(expression.variables) == operands.keys()
-
- scope = JointScope(FrozenDict(
- {operand_name: MappedScope(operand._scope, FrozenDict({operand_name: operand._expression}))
- for operand_name, operand in operands.items()}
- ))
- return cls(expression, scope)
-
- def __sub__(self, other: int):
- return type(self)(self._expression - other, self._scope)
-
- def __mul__(self, other: int):
- return type(self)(self._expression * other, self._scope)
-
-
-class VolatileRepetitionCount(VolatileValue):
- def __int__(self):
- value = self._expression.evaluate_in_scope(self._scope)
- if not is_integer(value):
- warnings.warn("Repetition count is no integer. Rounding might lead to unexpected results.")
- value = int(round(value))
- if value < 0:
- warnings.warn("Repetition count is negative. Clamping lead to unexpected results.")
- value = 0
- return value
-
- def update_volatile_dependencies(self, new_constants: Mapping[str, numbers.Number]) -> int:
- self._scope = self._scope.change_constants(new_constants)
- return int(self)
-
- def __eq__(self, other):
- if type(self) == type(other):
- return self._scope is other._scope and self._expression == other._expression
- else:
- return NotImplemented
+del qupulse
diff --git a/qupulse/_program/waveforms.py b/qupulse/_program/waveforms.py
index a173f3bf3..edd411cdd 100644
--- a/qupulse/_program/waveforms.py
+++ b/qupulse/_program/waveforms.py
@@ -1,1233 +1,13 @@
-"""This module contains all waveform classes
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
-Classes:
- - Waveform: An instantiated pulse which can be sampled to a raw voltage value array.
-"""
+"""Backwards compatibility link to qupulse.program.waveforms"""
-import itertools
-import operator
-import warnings
-from abc import ABCMeta, abstractmethod
-from numbers import Real
-from typing import (
- AbstractSet, Any, FrozenSet, Iterable, Mapping, NamedTuple, Sequence, Set,
- Tuple, Union, cast, Optional, List, Hashable)
-from weakref import WeakValueDictionary, ref
+from qupulse.program.waveforms import *
-import numpy as np
+import qupulse.program.waveforms
-from qupulse import ChannelID
-from qupulse._program.transformation import Transformation
-from qupulse.utils import checked_int_cast, isclose
-from qupulse.utils.types import TimeType, time_from_float
-from qupulse.utils.performance import is_monotonic
-from qupulse.comparable import Comparable
-from qupulse.expressions import ExpressionScalar
-from qupulse.pulses.interpolation import InterpolationStrategy
-from qupulse.utils import checked_int_cast, isclose
-from qupulse.utils.types import TimeType, time_from_float, FrozenDict
-from qupulse._program.transformation import Transformation
-from qupulse.utils import pairwise
+__all__ = qupulse.program.waveforms.__all__
-class ConstantFunctionPulseTemplateWarning(UserWarning):
- """ This warning indicates a constant waveform is constructed from a FunctionPulseTemplate """
- pass
-
-__all__ = ["Waveform", "TableWaveform", "TableWaveformEntry", "FunctionWaveform", "SequenceWaveform",
- "MultiChannelWaveform", "RepetitionWaveform", "TransformingWaveform", "ArithmeticWaveform",
- "ConstantFunctionPulseTemplateWarning"]
-
-PULSE_TO_WAVEFORM_ERROR = None # error margin in pulse template to waveform conversion
-
-# these are private because there probably will be changes here
-_ALLOCATION_FUNCTION = np.full_like # pre_allocated = ALLOCATION_FUNCTION(sample_times, **ALLOCATION_FUNCTION_KWARGS)
-_ALLOCATION_FUNCTION_KWARGS = dict(fill_value=np.nan, dtype=float)
-
-
-def _to_time_type(duration: Real) -> TimeType:
- if isinstance(duration, TimeType):
- return duration
- else:
- return time_from_float(float(duration), absolute_error=PULSE_TO_WAVEFORM_ERROR)
-
-
-class Waveform(Comparable, metaclass=ABCMeta):
- """Represents an instantiated PulseTemplate which can be sampled to retrieve arrays of voltage
- values for the hardware."""
-
- __sampled_cache = WeakValueDictionary()
-
- __slots__ = ('_duration',)
-
- def __init__(self, duration: TimeType):
- self._duration = duration
-
- @property
- def duration(self) -> TimeType:
- """The duration of the waveform in time units."""
- return self._duration
-
- @abstractmethod
- def unsafe_sample(self,
- channel: ChannelID,
- sample_times: np.ndarray,
- output_array: Union[np.ndarray, None] = None) -> np.ndarray:
- """Sample the waveform at given sample times.
-
- The unsafe means that there are no sanity checks performed. The provided sample times are assumed to be
- monotonously increasing and lie in the range of [0, waveform.duration]
-
- Args:
- sample_times: Times at which this Waveform will be sampled.
- output_array: Has to be either None or an array of the same size and type as sample_times. If
- not None, the sampled values will be written here and this array will be returned
- Result:
- The sampled values of this Waveform at the provided sample times. Has the same number of
- elements as sample_times.
- """
-
- def get_sampled(self,
- channel: ChannelID,
- sample_times: np.ndarray,
- output_array: Union[np.ndarray, None] = None) -> np.ndarray:
- """A wrapper to the unsafe_sample method which caches the result. This method enforces the constrains
- unsafe_sample expects and caches the result to save memory.
-
- Args:
- sample_times: Times at which this Waveform will be sampled.
- output_array: Has to be either None or an array of the same size and type as sample_times. If an array is
- given, the sampled values will be written into the given array and it will be returned. Otherwise, a new
- array will be created and cached to save memory.
-
- Result:
- The sampled values of this Waveform at the provided sample times. Is `output_array` if provided
- """
- if len(sample_times) == 0:
- if output_array is None:
- return np.zeros_like(sample_times, dtype=float)
- elif len(output_array) == len(sample_times):
- return output_array
- else:
- raise ValueError('Output array length and sample time length are different')
-
- if not is_monotonic(sample_times):
- raise ValueError('The sample times are not monotonously increasing')
- if sample_times[0] < 0 or sample_times[-1] > float(self.duration):
- raise ValueError(f'The sample times [{sample_times[0]}, ..., {sample_times[-1]}] are not in the range'
- f' [0, duration={float(self.duration)}]')
- if channel not in self.defined_channels:
- raise KeyError('Channel not defined in this waveform: {}'.format(channel))
-
- constant_value = self.constant_value(channel)
- if constant_value is None:
- if output_array is None:
- # cache the result to save memory
- result = self.unsafe_sample(channel, sample_times)
- result.flags.writeable = False
- key = hash(bytes(result))
- if key not in self.__sampled_cache:
- self.__sampled_cache[key] = result
- return self.__sampled_cache[key]
- else:
- if len(output_array) != len(sample_times):
- raise ValueError('Output array length and sample time length are different')
- # use the user provided memory
- return self.unsafe_sample(channel=channel,
- sample_times=sample_times,
- output_array=output_array)
- else:
- if output_array is None:
- output_array = np.full_like(sample_times, fill_value=constant_value, dtype=float)
- else:
- output_array[:] = constant_value
- return output_array
-
- @property
- @abstractmethod
- def defined_channels(self) -> AbstractSet[ChannelID]:
- """The channels this waveform should played on. Use
- :func:`~qupulse.pulses.instructions.get_measurement_windows` to get a waveform for a subset of these."""
-
- @abstractmethod
- def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> 'Waveform':
- """Unsafe version of :func:`~qupulse.pulses.instructions.get_measurement_windows`."""
-
- def get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> 'Waveform':
- """Get a waveform that only describes the channels contained in `channels`.
-
- Args:
- channels: A channel set the return value should confine to.
-
- Raises:
- KeyError: If `channels` is not a subset of the waveform's defined channels.
-
- Returns:
- A waveform with waveform.defined_channels == `channels`
- """
- if not channels <= self.defined_channels:
- raise KeyError('Channels not defined on waveform: {}'.format(channels))
- if channels == self.defined_channels:
- return self
- return self.unsafe_get_subset_for_channels(channels=channels)
-
- def is_constant(self) -> bool:
- """Convenience function to check if all channels are constant. The result is equal to
- `all(waveform.constant_value(ch) is not None for ch in waveform.defined_channels)` but might be more performant.
-
- Returns:
- True if all channels have constant values.
- """
- return self.constant_value_dict() is not None
-
- def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
- result = {ch: self.constant_value(ch) for ch in self.defined_channels}
- if None in result.values():
- return None
- else:
- return result
-
- def constant_value(self, channel: ChannelID) -> Optional[float]:
- """Checks if the requested channel has a constant value and returns it if so.
-
- Guarantee that this assertion passes for every t in waveform duration:
- >>> assert waveform.constant_value(channel) is None or waveform.constant_value(t) = waveform.get_sampled(channel, t)
-
- Args:
- channel: The channel to check
-
- Returns:
- None if there is no guarantee that the channel is constant. The value otherwise.
- """
- return None
-
- def __neg__(self):
- return FunctorWaveform.from_functor(self, {ch: np.negative for ch in self.defined_channels})
-
- def __pos__(self):
- return self
-
- def _sort_key_for_channels(self) -> Sequence[Tuple[str, int]]:
- """Makes reproducible sorting by defined channels possible"""
- return sorted((ch, 0) if isinstance(ch, str) else ('', ch) for ch in self.defined_channels)
-
- def reversed(self) -> 'Waveform':
- """Returns a reversed version of this waveform."""
- # We don't check for constness here because const waveforms are supposed to override this method
- return ReversedWaveform(self)
-
-
-class TableWaveformEntry(NamedTuple('TableWaveformEntry', [('t', Real),
- ('v', float),
- ('interp', InterpolationStrategy)])):
- def __init__(self, t: float, v: float, interp: InterpolationStrategy):
- if not callable(interp):
- raise TypeError('{} is neither callable nor of type InterpolationStrategy'.format(interp))
-
- def __repr__(self):
- return f'{type(self).__name__}(t={self.t!r}, v={self.v!r}, interp={self.interp!r})'
-
-
-class TableWaveform(Waveform):
- EntryInInit = Union[TableWaveformEntry, Tuple[float, float, InterpolationStrategy]]
-
- """Waveform obtained from instantiating a TablePulseTemplate."""
-
- __slots__ = ('_table', '_channel_id')
-
- def __init__(self,
- channel: ChannelID,
- waveform_table: Tuple[TableWaveformEntry, ...]) -> None:
- """Create a new TableWaveform instance.
-
- Args:
- waveform_table: A tuple of instantiated and validated table entries
- """
- if not isinstance(waveform_table, tuple):
- warnings.warn("Please use a tuple of TableWaveformEntry to construct TableWaveform directly",
- category=DeprecationWarning)
- waveform_table = self._validate_input(waveform_table)
-
- super().__init__(duration=_to_time_type(waveform_table[-1].t))
-
- self._table = waveform_table
- self._channel_id = channel
-
- @staticmethod
- def _validate_input(input_waveform_table: Sequence[EntryInInit]) -> Union[Tuple[Real, Real],
- List[TableWaveformEntry]]:
- """ Checks that:
- - the time is increasing,
- - there are at least two entries
-
- Optimizations:
- - removes subsequent entries with same time or voltage values.
- - checks if the complete waveform is constant. Returns a (duration, value) tuple if this is the case
-
- Raises:
- ValueError:
- - there are less than two entries
- - the entries are not ordered in time
- - Any time is negative
- - The total length is zero
-
- Returns:
- A list of de-duplicated table entries
- OR
- A (duration, value) tuple if the waveform is constant
- """
- # we use an iterator here to avoid duplicate work and be maximally efficient for short tables
- # We never use StopIteration to abort iteration. It always signifies an error.
- input_iter = iter(input_waveform_table)
- try:
- first_t, first_v, first_interp = next(input_iter)
- except StopIteration:
- raise ValueError("Waveform table mut not be empty")
-
- if first_t != 0.0:
- raise ValueError('First time entry is not zero.')
-
- previous_t = 0.0
- previous_v = first_v
- output_waveform_table = [TableWaveformEntry(0.0, first_v, first_interp)]
-
- try:
- t, v, interp = next(input_iter)
- except StopIteration:
- raise ValueError("Waveform table has less than two entries.")
- if t < 0:
- raise ValueError('Negative time values are not allowed.')
-
- # constant_v is None <=> the waveform is constant until up to the current entry
- constant_v = interp.constant_value((previous_t, previous_v), (t, v))
-
- for next_t, next_v, next_interp in input_iter:
- if next_t < t:
- if next_t < 0:
- raise ValueError('Negative time values are not allowed.')
- else:
- raise ValueError('Times are not increasing.')
-
- if constant_v is not None and interp.constant_value((t, v), (next_t, next_v)) != constant_v:
- constant_v = None
-
- if (previous_t != t or t != next_t) and (previous_v != v or v != next_v):
- # the time and the value differ both either from the next or the previous
- # otherwise we skip the entry
- previous_t = t
- previous_v = v
- output_waveform_table.append(TableWaveformEntry(t, v, interp))
-
- t, v, interp = next_t, next_v, next_interp
-
- # Until now, we only checked that the time does not decrease. We require an increase because duration == 0
- # waveforms are ill-formed. t is now the time of the last entry.
- if t == 0:
- raise ValueError('Last time entry is zero.')
-
- if constant_v is not None:
- # the waveform is constant
- return t, constant_v
- else:
- # we must still add the last entry to the table
- output_waveform_table.append(TableWaveformEntry(t, v, interp))
- return output_waveform_table
-
- def is_constant(self) -> bool:
- # only correct if `from_table` is used
- return False
-
- def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
- # only correct if `from_table` is used
- return None
-
- @classmethod
- def from_table(cls, channel: ChannelID, table: Sequence[EntryInInit]) -> Union['TableWaveform', 'ConstantWaveform']:
- table = cls._validate_input(table)
- if isinstance(table, tuple):
- duration, amplitude = table
- return ConstantWaveform(duration=duration, amplitude=amplitude, channel=channel)
- else:
- return TableWaveform(channel, tuple(table))
-
- @property
- def compare_key(self) -> Any:
- return self._channel_id, self._table
-
- def unsafe_sample(self,
- channel: ChannelID,
- sample_times: np.ndarray,
- output_array: Union[np.ndarray, None] = None) -> np.ndarray:
- if output_array is None:
- output_array = _ALLOCATION_FUNCTION(sample_times, **_ALLOCATION_FUNCTION_KWARGS)
-
- if PULSE_TO_WAVEFORM_ERROR:
- # we need to replace the last entry's t with self.duration
- *entries, last = self._table
- entries.append(TableWaveformEntry(float(self.duration), last.v, last.interp))
- else:
- entries = self._table
-
- for entry1, entry2 in pairwise(entries):
- indices = slice(np.searchsorted(sample_times, entry1.t, 'left'),
- np.searchsorted(sample_times, entry2.t, 'right'))
- output_array[indices] = \
- entry2.interp((float(entry1.t), entry1.v),
- (float(entry2.t), entry2.v),
- sample_times[indices])
- return output_array
-
- @property
- def defined_channels(self) -> AbstractSet[ChannelID]:
- return {self._channel_id}
-
- def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> 'Waveform':
- return self
-
- def __repr__(self):
- return f'{type(self).__name__}(channel={self._channel_id!r}, waveform_table={self._table!r})'
-
-
-class ConstantWaveform(Waveform):
-
- # TODO: remove
- _is_constant_waveform = True
-
- __slots__ = ('_amplitude', '_channel')
-
- def __init__(self, duration: Real, amplitude: Any, channel: ChannelID):
- """ Create a qupulse waveform corresponding to a ConstantPulseTemplate """
- super().__init__(duration=_to_time_type(duration))
- self._amplitude = amplitude
- self._channel = channel
-
- @classmethod
- def from_mapping(cls, duration: Real, constant_values: Mapping[ChannelID, float]) -> Union['ConstantWaveform',
- 'MultiChannelWaveform']:
- """Construct a ConstantWaveform or a MultiChannelWaveform of ConstantWaveforms with given duration and values"""
- assert constant_values
- duration = _to_time_type(duration)
- if len(constant_values) == 1:
- (channel, amplitude), = constant_values.items()
- return cls(duration, amplitude=amplitude, channel=channel)
- else:
- return MultiChannelWaveform([cls(duration, amplitude=amplitude, channel=channel)
- for channel, amplitude in constant_values.items()])
-
- def is_constant(self) -> bool:
- return True
-
- def constant_value(self, channel: ChannelID) -> Optional[float]:
- assert channel == self._channel
- return self._amplitude
-
- def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
- return {self._channel: self._amplitude}
-
- @property
- def defined_channels(self) -> AbstractSet[ChannelID]:
- """The channels this waveform should played on. Use
- :func:`~qupulse.pulses.instructions.get_measurement_windows` to get a waveform for a subset of these."""
-
- return {self._channel}
-
- @property
- def compare_key(self) -> Tuple[Any, ...]:
- return self._duration, self._amplitude, self._channel
-
- def unsafe_sample(self,
- channel: ChannelID,
- sample_times: np.ndarray,
- output_array: Union[np.ndarray, None] = None) -> np.ndarray:
- if output_array is None:
- return np.full_like(sample_times, fill_value=self._amplitude, dtype=float)
- else:
- output_array[:] = self._amplitude
- return output_array
-
- def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> Waveform:
- """Unsafe version of :func:`~qupulse.pulses.instructions.get_measurement_windows`."""
- return self
-
- def __repr__(self):
- return f"{type(self).__name__}(duration={self.duration!r}, "\
- f"amplitude={self._amplitude!r}, channel={self._channel!r})"
-
- def reversed(self) -> 'Waveform':
- return self
-
-
-class FunctionWaveform(Waveform):
- """Waveform obtained from instantiating a FunctionPulseTemplate."""
-
- __slots__ = ('_expression', '_channel_id')
-
- def __init__(self, expression: ExpressionScalar,
- duration: float,
- channel: ChannelID) -> None:
- """Creates a new FunctionWaveform instance.
-
- Args:
- expression: The function represented by this FunctionWaveform
- as a mathematical expression where 't' denotes the time variable. It must not have other variables
- duration: The duration of the waveform
- measurement_windows: A list of measurement windows
- channel: The channel this waveform is played on
- """
-
- if set(expression.variables) - set('t'):
- raise ValueError('FunctionWaveforms may not depend on anything but "t"')
- elif not expression.variables:
- warnings.warn("Constant FunctionWaveform is not recommended as the constant propagation will be suboptimal",
- category=ConstantFunctionPulseTemplateWarning)
- super().__init__(duration=_to_time_type(duration))
- self._expression = expression
- self._channel_id = channel
-
- @classmethod
- def from_expression(cls, expression: ExpressionScalar, duration: float, channel: ChannelID) -> Union['FunctionWaveform', ConstantWaveform]:
- if expression.variables:
- return cls(expression, duration, channel)
- else:
- return ConstantWaveform(amplitude=expression.evaluate_numeric(), duration=duration, channel=channel)
-
- def is_constant(self) -> bool:
- # only correct if `from_expression` is used
- return False
-
- def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
- # only correct if `from_expression` is used
- return None
-
- @property
- def defined_channels(self) -> AbstractSet[ChannelID]:
- return {self._channel_id}
-
- @property
- def compare_key(self) -> Any:
- return self._channel_id, self._expression, self._duration
-
- @property
- def duration(self) -> TimeType:
- return self._duration
-
- def unsafe_sample(self,
- channel: ChannelID,
- sample_times: np.ndarray,
- output_array: Union[np.ndarray, None] = None) -> np.ndarray:
- evaluated = self._expression.evaluate_numeric(t=sample_times)
- if output_array is None:
- if self._expression.variables:
- return evaluated.astype(float)
- else:
- return np.full_like(sample_times, fill_value=float(evaluated), dtype=float)
- else:
- output_array[:] = evaluated
- return output_array
-
- def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> Waveform:
- return self
-
- def __repr__(self):
- return f"{type(self).__name__}(duration={self.duration!r}, "\
- f"expression={self._expression!r}, channel={self._channel_id!r})"
-
-
-class SequenceWaveform(Waveform):
- """This class allows putting multiple PulseTemplate together in one waveform on the hardware."""
-
- __slots__ = ('_sequenced_waveforms', )
-
- def __init__(self, sub_waveforms: Iterable[Waveform]):
- """Use Waveform.from_sequence for optimal construction
-
- :param subwaveforms: All waveforms must have the same defined channels
- """
- if not sub_waveforms:
- raise ValueError(
- "SequenceWaveform cannot be constructed without channel waveforms."
- )
-
- # do not fail on iterators although we do not allow them as an argument
- sequenced_waveforms = tuple(sub_waveforms)
-
- super().__init__(duration=sum(waveform.duration for waveform in sequenced_waveforms))
- self._sequenced_waveforms = sequenced_waveforms
-
- defined_channels = self._sequenced_waveforms[0].defined_channels
- if not all(waveform.defined_channels == defined_channels
- for waveform in itertools.islice(self._sequenced_waveforms, 1, None)):
- for waveform in self._sequenced_waveforms[1:]:
- if not waveform.defined_channels == self.defined_channels:
- print(f"SequenceWaveform: defined channels {self.defined_channels} do not match {waveform.defined_channels} ")
- raise ValueError(
- "SequenceWaveform cannot be constructed from waveforms of different"
- "defined channels."
- )
-
- @classmethod
- def from_sequence(cls, waveforms: Sequence['Waveform']) -> 'Waveform':
- """Returns a waveform the represents the given sequence of waveforms. Applies some optimizations."""
- assert waveforms, "Sequence must not be empty"
- if len(waveforms) == 1:
- return waveforms[0]
-
- flattened = []
- constant_values = waveforms[0].constant_value_dict()
- for wf in waveforms:
- if constant_values and constant_values != wf.constant_value_dict():
- constant_values = None
- if isinstance(wf, cls):
- flattened.extend(wf.sequenced_waveforms)
- else:
- flattened.append(wf)
- if constant_values is None:
- return cls(sub_waveforms=flattened)
- else:
- duration = sum(wf.duration for wf in flattened)
- return ConstantWaveform.from_mapping(duration, constant_values)
-
- def is_constant(self) -> bool:
- # only correct if from_sequence is used for construction
- return False
-
- def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
- # only correct if from_sequence is used for construction
- return None
-
- def constant_value(self, channel: ChannelID) -> Optional[float]:
- v = None
- for wf in self._sequenced_waveforms:
- wf_cv = wf.constant_value(channel)
- if wf_cv is None:
- return None
- elif wf_cv == v:
- continue
- elif v is None:
- v = wf_cv
- else:
- assert v != wf_cv
- return None
- return v
-
- @property
- def defined_channels(self) -> AbstractSet[ChannelID]:
- return self._sequenced_waveforms[0].defined_channels
-
- def unsafe_sample(self,
- channel: ChannelID,
- sample_times: np.ndarray,
- output_array: Union[np.ndarray, None] = None) -> np.ndarray:
- if output_array is None:
- output_array = _ALLOCATION_FUNCTION(sample_times, **_ALLOCATION_FUNCTION_KWARGS)
- time = 0
- for subwaveform in self._sequenced_waveforms:
- # before you change anything here, make sure to understand the difference between basic and advanced
- # indexing in numpy and their copy/reference behaviour
- end = time + subwaveform.duration
-
- indices = slice(*np.searchsorted(sample_times, (float(time), float(end)), 'left'))
- subwaveform.unsafe_sample(channel=channel,
- sample_times=sample_times[indices]-np.float64(time),
- output_array=output_array[indices])
- time = end
- return output_array
-
- @property
- def compare_key(self) -> Tuple[Waveform]:
- return self._sequenced_waveforms
-
- @property
- def duration(self) -> TimeType:
- return self._duration
-
- def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> 'Waveform':
- return SequenceWaveform.from_sequence([
- sub_waveform.unsafe_get_subset_for_channels(channels & sub_waveform.defined_channels)
- for sub_waveform in self._sequenced_waveforms if sub_waveform.defined_channels & channels])
-
- @property
- def sequenced_waveforms(self) -> Sequence[Waveform]:
- return self._sequenced_waveforms
-
- def __repr__(self):
- return f"{type(self).__name__}({self._sequenced_waveforms})"
-
-
-class MultiChannelWaveform(Waveform):
- """A MultiChannelWaveform is a Waveform object that allows combining arbitrary Waveform objects
- to into a single waveform defined for several channels.
-
- The number of channels used by the MultiChannelWaveform object is the sum of the channels used
- by the Waveform objects it consists of.
-
- MultiChannelWaveform allows an arbitrary mapping of channels defined by the Waveforms it
- consists of and the channels it defines. For example, if the MultiChannelWaveform consists
- of a two Waveform objects A and B which define two channels each, then the channels of the
- MultiChannelWaveform may be 0: A.1, 1: B.0, 2: B.1, 3: A.0 where A.0 means channel 0 of Waveform
- object A.
-
- The following constraints must hold:
- - The durations of all Waveform objects must be equal.
- - The channel mapping must be sane, i.e., no channel of the MultiChannelWaveform must be
- assigned more than one channel of any Waveform object it consists of
- """
-
- __slots__ = ('_sub_waveforms', '_defined_channels')
-
- def __init__(self, sub_waveforms: List[Waveform]) -> None:
- """Create a new MultiChannelWaveform instance.
- Use `MultiChannelWaveform.from_parallel` for optimal construction.
-
- Requires a list of subwaveforms in the form (Waveform, List(int)) where the list defines
- the channel mapping, i.e., a value y at index x in the list means that channel x of the
- subwaveform will be mapped to channel y of this MultiChannelWaveform object.
-
- Args:
- sub_waveforms: The list of sub waveforms of this
- MultiChannelWaveform. List might get sorted!
- Raises:
- ValueError, if a channel mapping is out of bounds of the channels defined by this
- MultiChannelWaveform
- ValueError, if several subwaveform channels are assigned to a single channel of this
- MultiChannelWaveform
- ValueError, if subwaveforms have inconsistent durations
- """
-
- if not sub_waveforms:
- raise ValueError(
- "MultiChannelWaveform cannot be constructed without channel waveforms."
- )
-
- # sort the waveforms with their defined channels to make compare key reproducible
- if not isinstance(sub_waveforms, list):
- sub_waveforms = list(sub_waveforms)
- sub_waveforms.sort(key=lambda wf: wf._sort_key_for_channels())
-
- super().__init__(duration=sub_waveforms[0].duration)
- self._sub_waveforms = tuple(sub_waveforms)
-
- defined_channels = set()
- for waveform in self._sub_waveforms:
- if waveform.defined_channels & defined_channels:
- raise ValueError('Channel may not be defined in multiple waveforms',
- waveform.defined_channels & defined_channels)
- defined_channels |= waveform.defined_channels
- self._defined_channels = frozenset(defined_channels)
-
- if not all(isclose(waveform.duration, self.duration) for waveform in self._sub_waveforms[1:]):
- # meaningful error message:
- durations = {}
-
- for waveform in self._sub_waveforms:
- for duration, channels in durations.items():
- if isclose(waveform.duration, duration):
- channels.update(waveform.defined_channels)
- break
- else:
- durations[waveform.duration] = set(waveform.defined_channels)
-
- raise ValueError(
- "MultiChannelWaveform cannot be constructed from channel waveforms of different durations.",
- durations
- )
-
- @staticmethod
- def from_parallel(waveforms: Sequence[Waveform]) -> Waveform:
- assert waveforms, "ARgument must not be empty"
- if len(waveforms) == 1:
- return waveforms[0]
-
- # we do not look at constant values here because there is no benefit. We would need to construct a new
- # MultiChannelWaveform anyways
-
- # avoid unnecessary multi channel nesting
- flattened = []
- for waveform in waveforms:
- if isinstance(waveform, MultiChannelWaveform):
- flattened.extend(waveform._sub_waveforms)
- else:
- flattened.append(waveform)
-
- return MultiChannelWaveform(flattened)
-
- def is_constant(self) -> bool:
- return all(wf.is_constant() for wf in self._sub_waveforms)
-
- def constant_value(self, channel: ChannelID) -> Optional[float]:
- return self[channel].constant_value(channel)
-
- def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
- d = {}
- for wf in self._sub_waveforms:
- wf_d = wf.constant_value_dict()
- if wf_d is None:
- return None
- else:
- d.update(wf_d)
- return d
-
- @property
- def duration(self) -> TimeType:
- return self._sub_waveforms[0].duration
-
- def __getitem__(self, key: ChannelID) -> Waveform:
- for waveform in self._sub_waveforms:
- if key in waveform.defined_channels:
- return waveform
- raise KeyError('Unknown channel ID: {}'.format(key), key)
-
- @property
- def defined_channels(self) -> AbstractSet[ChannelID]:
- return self._defined_channels
-
- @property
- def compare_key(self) -> Any:
- # sort with channels
- return self._sub_waveforms
-
- def unsafe_sample(self,
- channel: ChannelID,
- sample_times: np.ndarray,
- output_array: Union[np.ndarray, None] = None) -> np.ndarray:
- return self[channel].unsafe_sample(channel, sample_times, output_array)
-
- def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> 'Waveform':
- relevant_sub_waveforms = [swf for swf in self._sub_waveforms if swf.defined_channels & channels]
- if len(relevant_sub_waveforms) == 1:
- return relevant_sub_waveforms[0].get_subset_for_channels(channels)
- elif len(relevant_sub_waveforms) > 1:
- return MultiChannelWaveform.from_parallel(
- [sub_waveform.get_subset_for_channels(channels & sub_waveform.defined_channels)
- for sub_waveform in relevant_sub_waveforms])
- else:
- raise KeyError('Unknown channels: {}'.format(channels))
-
- def __repr__(self):
- return f"{type(self).__name__}({self._sub_waveforms!r})"
-
-
-class RepetitionWaveform(Waveform):
- """This class allows putting multiple PulseTemplate together in one waveform on the hardware."""
-
- __slots__ = ('_body', '_repetition_count')
-
- def __init__(self, body: Waveform, repetition_count: int):
- repetition_count = checked_int_cast(repetition_count)
- if repetition_count < 1 or not isinstance(repetition_count, int):
- raise ValueError('Repetition count must be an integer >0')
-
- super().__init__(duration=body.duration * repetition_count)
- self._body = body
- self._repetition_count = repetition_count
-
- @classmethod
- def from_repetition_count(cls, body: Waveform, repetition_count: int) -> Waveform:
- constant_values = body.constant_value_dict()
- if constant_values is None:
- return RepetitionWaveform(body, repetition_count)
- else:
- return ConstantWaveform.from_mapping(body.duration * repetition_count, constant_values)
-
- @property
- def defined_channels(self) -> AbstractSet[ChannelID]:
- return self._body.defined_channels
-
- def unsafe_sample(self,
- channel: ChannelID,
- sample_times: np.ndarray,
- output_array: Union[np.ndarray, None] = None) -> np.ndarray:
- if output_array is None:
- output_array = _ALLOCATION_FUNCTION(sample_times, **_ALLOCATION_FUNCTION_KWARGS)
- body_duration = self._body.duration
- time = 0
- for _ in range(self._repetition_count):
- end = time + body_duration
- indices = slice(*np.searchsorted(sample_times, (float(time), float(end)), 'left'))
- self._body.unsafe_sample(channel=channel,
- sample_times=sample_times[indices] - float(time),
- output_array=output_array[indices])
- time = end
- return output_array
-
- @property
- def compare_key(self) -> Tuple[Any, int]:
- return self._body.compare_key, self._repetition_count
-
- def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> Waveform:
- return RepetitionWaveform.from_repetition_count(
- body=self._body.unsafe_get_subset_for_channels(channels),
- repetition_count=self._repetition_count)
-
- def is_constant(self) -> bool:
- return self._body.is_constant()
-
- def constant_value(self, channel: ChannelID) -> Optional[float]:
- return self._body.constant_value(channel)
-
- def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
- return self._body.constant_value_dict()
-
- def __repr__(self):
- return f"{type(self).__name__}(body={self._body!r}, repetition_count={self._repetition_count!r})"
-
-
-class TransformingWaveform(Waveform):
- __slots__ = ('_inner_waveform', '_transformation', '_cached_data', '_cached_times')
-
- def __init__(self, inner_waveform: Waveform, transformation: Transformation):
- """"""
- super(TransformingWaveform, self).__init__(duration=inner_waveform.duration)
- self._inner_waveform = inner_waveform
- self._transformation = transformation
-
- # cache data of inner channels based identified and invalidated by the sample times
- self._cached_data = None
- self._cached_times = lambda: None
-
- @classmethod
- def from_transformation(cls, inner_waveform: Waveform, transformation: Transformation) -> Waveform:
- constant_values = inner_waveform.constant_value_dict()
-
- if constant_values is None or not transformation.is_constant_invariant():
- return cls(inner_waveform, transformation)
-
- transformed_constant_values = {key: float(value) for key, value in transformation(0., constant_values).items()}
- return ConstantWaveform.from_mapping(inner_waveform.duration, transformed_constant_values)
-
- def is_constant(self) -> bool:
- # only true if `from_transformation` was used
- return False
-
- def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
- # only true if `from_transformation` was used
- return None
-
- def constant_value(self, channel: ChannelID) -> Optional[float]:
- if not self._transformation.is_constant_invariant():
- return None
- in_channels = self._transformation.get_input_channels({channel})
- in_values = {ch: self._inner_waveform.constant_value(ch) for ch in in_channels}
- if any(val is None for val in in_values.values()):
- return None
- else:
- return self._transformation(0., in_values)[channel]
-
- @property
- def inner_waveform(self) -> Waveform:
- return self._inner_waveform
-
- @property
- def transformation(self) -> Transformation:
- return self._transformation
-
- @property
- def defined_channels(self) -> AbstractSet[ChannelID]:
- return self.transformation.get_output_channels(self.inner_waveform.defined_channels)
-
- @property
- def compare_key(self) -> Tuple[Waveform, Transformation]:
- return self.inner_waveform, self.transformation
-
- def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> 'SubsetWaveform':
- return SubsetWaveform(self, channel_subset=channels)
-
- def unsafe_sample(self,
- channel: ChannelID,
- sample_times: np.ndarray,
- output_array: Union[np.ndarray, None] = None) -> np.ndarray:
- if self._cached_times() is not sample_times:
- self._cached_data = dict()
- self._cached_times = ref(sample_times)
-
- if channel not in self._cached_data:
-
- inner_channels = self.transformation.get_input_channels({channel})
-
- inner_data = {inner_channel: self.inner_waveform.unsafe_sample(inner_channel, sample_times)
- for inner_channel in inner_channels}
-
- outer_data = self.transformation(sample_times, inner_data)
- self._cached_data.update(outer_data)
-
- if output_array is None:
- output_array = self._cached_data[channel]
- else:
- output_array[:] = self._cached_data[channel]
-
- return output_array
-
-
-class SubsetWaveform(Waveform):
- __slots__ = ('_inner_waveform', '_channel_subset')
-
- def __init__(self, inner_waveform: Waveform, channel_subset: Set[ChannelID]):
- super().__init__(duration=inner_waveform.duration)
- self._inner_waveform = inner_waveform
- self._channel_subset = frozenset(channel_subset)
-
- @property
- def inner_waveform(self) -> Waveform:
- return self._inner_waveform
-
- @property
- def defined_channels(self) -> FrozenSet[ChannelID]:
- return self._channel_subset
-
- @property
- def compare_key(self) -> Tuple[frozenset, Waveform]:
- return self.defined_channels, self.inner_waveform
-
- def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> Waveform:
- return self.inner_waveform.get_subset_for_channels(channels)
-
- def unsafe_sample(self,
- channel: ChannelID,
- sample_times: np.ndarray,
- output_array: Union[np.ndarray, None] = None) -> np.ndarray:
- return self.inner_waveform.unsafe_sample(channel, sample_times, output_array)
-
- def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
- d = self._inner_waveform.constant_value_dict()
- if d is not None:
- return {ch: d[ch] for ch in self._channel_subset}
-
- def constant_value(self, channel: ChannelID) -> Optional[float]:
- if channel not in self._channel_subset:
- raise KeyError(channel)
- return self._inner_waveform.constant_value(channel)
-
-
-class ArithmeticWaveform(Waveform):
- """Channels only present in one waveform have the operations neutral element on the other."""
-
- numpy_operator_map = {'+': np.add,
- '-': np.subtract}
- operator_map = {'+': operator.add,
- '-': operator.sub}
-
- rhs_only_map = {'+': operator.pos,
- '-': operator.neg}
- numpy_rhs_only_map = {'+': np.positive,
- '-': np.negative}
-
- __slots__ = ('_lhs', '_rhs', '_arithmetic_operator')
-
- def __init__(self,
- lhs: Waveform,
- arithmetic_operator: str,
- rhs: Waveform):
- super().__init__(duration=lhs.duration)
- self._lhs = lhs
- self._rhs = rhs
- self._arithmetic_operator = arithmetic_operator
-
- assert np.isclose(float(self._lhs.duration), float(self._rhs.duration))
- assert arithmetic_operator in self.operator_map
-
- @classmethod
- def from_operator(cls, lhs: Waveform, arithmetic_operator: str, rhs: Waveform):
- # one could optimize rhs_cv to being only created if lhs_cv is not None but this makes the code harder to read
- lhs_cv = lhs.constant_value_dict()
- rhs_cv = rhs.constant_value_dict()
- if lhs_cv is None or rhs_cv is None:
- return cls(lhs, arithmetic_operator, rhs)
-
- else:
- constant_values = dict(lhs_cv)
- op = cls.operator_map[arithmetic_operator]
- rhs_op = cls.rhs_only_map[arithmetic_operator]
-
- for ch, rhs_val in rhs_cv.items():
- if ch in constant_values:
- constant_values[ch] = op(constant_values[ch], rhs_val)
- else:
- constant_values[ch] = rhs_op(rhs_val)
-
- duration = lhs.duration
- assert isclose(duration, rhs.duration)
-
- return ConstantWaveform.from_mapping(duration, constant_values)
-
- def constant_value(self, channel: ChannelID) -> Optional[float]:
- if channel not in self._rhs.defined_channels:
- return self._lhs.constant_value(channel)
- rhs = self._rhs.constant_value(channel)
- if rhs is None:
- return None
-
- if channel in self._lhs.defined_channels:
- lhs = self._lhs.constant_value(channel)
- if lhs is None:
- return None
-
- return self.operator_map[self._arithmetic_operator](lhs, rhs)
- else:
- return self.rhs_only_map[self._arithmetic_operator](rhs)
-
- def is_constant(self) -> bool:
- # only correct if from_operator is used
- return False
-
- def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
- # only correct if from_operator is used
- return None
-
- @property
- def lhs(self) -> Waveform:
- return self._lhs
-
- @property
- def rhs(self) -> Waveform:
- return self._rhs
-
- @property
- def arithmetic_operator(self) -> str:
- return self._arithmetic_operator
-
- @property
- def duration(self) -> TimeType:
- return self._lhs.duration
-
- @property
- def defined_channels(self) -> AbstractSet[ChannelID]:
- return self._lhs.defined_channels | self._rhs.defined_channels
-
- def unsafe_sample(self,
- channel: ChannelID,
- sample_times: np.ndarray,
- output_array: Union[np.ndarray, None] = None) -> np.ndarray:
- if channel in self._lhs.defined_channels:
- lhs = self._lhs.unsafe_sample(channel=channel, sample_times=sample_times, output_array=output_array)
- else:
- lhs = None
-
- if channel in self._rhs.defined_channels:
- rhs = self._rhs.unsafe_sample(channel=channel, sample_times=sample_times,
- output_array=None if lhs is not None else output_array)
- else:
- rhs = None
-
- if rhs is not None and lhs is not None:
- arithmetic_operator = self.numpy_operator_map[self._arithmetic_operator]
- if output_array is None:
- output_array = lhs
- return arithmetic_operator(lhs, rhs, out=output_array)
-
- else:
- if lhs is None:
- assert rhs is not None, "channel %r not in defined channels (internal bug)" % channel
- return self.numpy_rhs_only_map[self._arithmetic_operator](rhs, out=output_array)
- else:
- return lhs
-
- def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> Waveform:
- # TODO: optimization possible
- return SubsetWaveform(self, channels)
-
- @property
- def compare_key(self) -> Tuple[str, Waveform, Waveform]:
- return self._arithmetic_operator, self._lhs, self._rhs
-
-
-class FunctorWaveform(Waveform):
- # TODO: Use Protocol to enforce that it accepts second argument has the keyword out
- Functor = callable
-
- __slots__ = ('_inner_waveform', '_functor')
-
- """Apply a channel wise functor that works inplace to all results. The functor must accept two arguments"""
- def __init__(self, inner_waveform: Waveform, functor: Mapping[ChannelID, Functor]):
- super(FunctorWaveform, self).__init__(duration=inner_waveform.duration)
- self._inner_waveform = inner_waveform
- self._functor = dict(functor.items())
-
- assert set(functor.keys()) == inner_waveform.defined_channels, ("There is no default identity mapping (yet)."
- "File an issue on github if you need it.")
-
- @classmethod
- def from_functor(cls, inner_waveform: Waveform, functor: Mapping[ChannelID, Functor]):
- constant_values = inner_waveform.constant_value_dict()
- if constant_values is None:
- return FunctorWaveform(inner_waveform, functor)
-
- funced_constant_values = {ch: functor[ch](val) for ch, val in constant_values.items()}
- return ConstantWaveform.from_mapping(inner_waveform.duration, funced_constant_values)
-
- def is_constant(self) -> bool:
- # only correct if `from_functor` was used
- return False
-
- def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
- # only correct if `from_functor` was used
- return None
-
- def constant_value(self, channel: ChannelID) -> Optional[float]:
- inner = self._inner_waveform.constant_value(channel)
- if inner is None:
- return None
- else:
- return self._functor[channel](inner)
-
- @property
- def defined_channels(self) -> AbstractSet[ChannelID]:
- return self._inner_waveform.defined_channels
-
- def unsafe_sample(self,
- channel: ChannelID,
- sample_times: np.ndarray,
- output_array: Union[np.ndarray, None] = None) -> np.ndarray:
- inner_output = self._inner_waveform.unsafe_sample(channel, sample_times, output_array)
- return self._functor[channel](inner_output, out=inner_output)
-
- def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> Waveform:
- return FunctorWaveform.from_functor(
- self._inner_waveform.unsafe_get_subset_for_channels(channels),
- {ch: self._functor[ch] for ch in channels})
-
- @property
- def compare_key(self) -> Tuple[Waveform, FrozenSet]:
- return self._inner_waveform, frozenset(self._functor.items())
-
-
-class ReversedWaveform(Waveform):
- """Reverses the inner waveform in time."""
-
- __slots__ = ('_inner',)
-
- def __init__(self, inner: Waveform):
- super().__init__(duration=inner.duration)
- self._inner = inner
-
- @classmethod
- def from_to_reverse(cls, inner: Waveform) -> Waveform:
- if inner.constant_value_dict():
- return inner
- else:
- return cls(inner)
-
- def unsafe_sample(self, channel: ChannelID, sample_times: np.ndarray,
- output_array: Union[np.ndarray, None] = None) -> np.ndarray:
- inner_sample_times = (float(self.duration) - sample_times)[::-1]
- if output_array is None:
- return self._inner.unsafe_sample(channel, inner_sample_times, None)[::-1]
- else:
- inner_output_array = output_array[::-1]
- inner_output_array = self._inner.unsafe_sample(channel, inner_sample_times, output_array=inner_output_array)
- if inner_output_array.base not in (output_array, output_array.base):
- # TODO: is there a guarantee by numpy we never end up here?
- output_array[:] = inner_output_array[::-1]
- return output_array
-
- @property
- def defined_channels(self) -> AbstractSet[ChannelID]:
- return self._inner.defined_channels
-
- def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> 'Waveform':
- return ReversedWaveform.from_to_reverse(self._inner.unsafe_get_subset_for_channels(channels))
-
- @property
- def compare_key(self) -> Hashable:
- return self._inner.compare_key
-
- def reversed(self) -> 'Waveform':
- return self._inner
+del qupulse
diff --git a/qupulse/comparable.py b/qupulse/comparable.py
deleted file mode 100644
index 2582faa8b..000000000
--- a/qupulse/comparable.py
+++ /dev/null
@@ -1,40 +0,0 @@
-"""This module defines the abstract Comparable class."""
-from abc import abstractmethod
-from typing import Hashable, Any
-
-from qupulse.utils.types import DocStringABCMeta
-
-
-__all__ = ["Comparable"]
-
-
-class Comparable(metaclass=DocStringABCMeta):
- """An object that can be queried for equality with other Comparable objects.
-
- Subclasses must override the abstract property _compare_key which shall provide some object
- natively equatable in Python (e.g., strings, numbers, tuples containing those, etc..).
- Comparable provides implementations of the hashing function as well as the equals and not-equals
- operators based on comparison of this key.
- """
- __slots__ = ()
-
- @property
- @abstractmethod
- def compare_key(self) -> Hashable:
- """Return a unique key used in comparison and hashing operations.
-
- The key must describe the essential properties of the object.
- Two objects are equal iff their keys are identical.
- """
-
- def __hash__(self) -> int:
- """Return a hash value of this Comparable object."""
- return hash(self.compare_key)
-
- def __eq__(self, other: Any) -> bool:
- """True, if other is equal to this Comparable object."""
- return isinstance(other, self.__class__) and self.compare_key == other.compare_key
-
- def __ne__(self, other: Any) -> bool:
- """True, if other is not equal to this Comparable object."""
- return not self == other
diff --git a/qupulse/examples/VolatileParameters.py b/qupulse/examples/VolatileParameters.py
deleted file mode 100644
index 53dcc3dab..000000000
--- a/qupulse/examples/VolatileParameters.py
+++ /dev/null
@@ -1,89 +0,0 @@
-from qupulse.hardware.setup import HardwareSetup, PlaybackChannel, MarkerChannel
-from qupulse.pulses import PointPT, RepetitionPT, TablePT
-
-
-#%%
-""" Connect and setup to your AWG. Change awg_address to the address of your awg and awg_name to the name of
-your AWGs manufacturer (Zürich Instruments: ZI, TaborElectronics: Tabor).
-"""
-
-awg_name = 'TABOR'
-awg_address = '127.0.0.1'
-
-hardware_setup = HardwareSetup()
-
-if awg_name == 'ZI':
- from qupulse.hardware.awgs.zihdawg import HDAWGRepresentation
- awg = HDAWGRepresentation(awg_address, 'USB')
-
- channel_pairs = []
- for pair_name in ('AB', 'CD', 'EF', 'GH'):
- channel_pair = getattr(awg, 'channel_pair_%s' % pair_name)
-
- for ch_i, ch_name in enumerate(pair_name):
- playback_name = '{name}_{ch_name}'.format(name=awg_name, ch_name=ch_name)
- hardware_setup.set_channel(playback_name,
- PlaybackChannel(channel_pair, ch_i))
- hardware_setup.set_channel(playback_name + '_MARKER_FRONT', MarkerChannel(channel_pair, 2 * ch_i))
- hardware_setup.set_channel(playback_name + '_MARKER_BACK', MarkerChannel(channel_pair, 2 * ch_i + 1))
- awg_channel = awg.channel_pair_AB
-
-elif awg_name == 'TABOR':
- from qupulse.hardware.awgs.tabor import TaborAWGRepresentation
- awg = TaborAWGRepresentation(awg_address, reset=True)
-
- channel_pairs = []
- for pair_name in ('AB', 'CD'):
- channel_pair = getattr(awg, 'channel_pair_%s' % pair_name)
- channel_pairs.append(channel_pair)
-
- for ch_i, ch_name in enumerate(pair_name):
- playback_name = '{name}_{ch_name}'.format(name=awg_name, ch_name=ch_name)
- hardware_setup.set_channel(playback_name, PlaybackChannel(channel_pair, ch_i))
- hardware_setup.set_channel(playback_name + '_MARKER', MarkerChannel(channel_pair, ch_i))
- awg_channel = channel_pairs[0]
-
-else:
- ValueError('Unknown AWG')
-
-#%%
-""" Create three simple pulses and put them together to a PulseTemplate called dnp """
-
-plus = [(0, 0), ('ta', 'va', 'hold'), ('tb', 'vb', 'linear'), ('tend', 0, 'jump')]
-minus = [(0, 0), ('ta', '-va', 'hold'), ('tb', '-vb', 'linear'), ('tend', 0, 'jump')]
-
-zero_pulse = PointPT([(0, 0), ('tend', 0)], ('X', 'Y'))
-plus_pulse = TablePT(entries={'X': plus, 'Y': plus})
-minus_pulse = TablePT(entries={'X': minus, 'Y': minus})
-
-dnp = RepetitionPT(minus_pulse, 'n_minus') @ RepetitionPT(zero_pulse, 'n_zero') @ RepetitionPT(plus_pulse, 'n_plus')
-
-#%%
-""" Create a program dnp with the number of pulse repetitions as volatile parameters """
-
-sample_rate = awg_channel.sample_rate / 10**9
-n_quant = 192
-t_quant = n_quant / sample_rate
-
-dnp_prog = dnp.create_program(parameters=dict(tend=float(t_quant), ta=float(t_quant/3), tb=float(2*t_quant/3),
- va=0.12, vb=0.25, n_minus=3, n_zero=3, n_plus=3),
- channel_mapping={'X': '{}_A'.format(awg_name), 'Y': '{}_B'.format(awg_name)},
- volatile={'n_minus', 'n_zero', 'n_plus'})
-dnp_prog.cleanup()
-
-#%%
-""" Upload this program to the AWG """
-
-hardware_setup.register_program('dnp', dnp_prog)
-hardware_setup.arm_program('dnp')
-
-#%%
-""" Run initial program """
-
-awg_channel.run_current_program()
-
-#%%
-""" Change volatile parameters to new values and run the modified program """
-
-hardware_setup.update_parameters('dnp', dict(n_zero=1, n_plus=5))
-awg_channel.run_current_program()
diff --git a/qupulse/expressions/__init__.py b/qupulse/expressions/__init__.py
new file mode 100644
index 000000000..0c39e8f6f
--- /dev/null
+++ b/qupulse/expressions/__init__.py
@@ -0,0 +1,80 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+"""This subpackage contains qupulse's expression logic. The submodule :py:mod:`.expressions.protocol` defines the :py:class:`typing.Protocol`
+that expression functionality providers must implement. This allows to substitute the powerful and expressive but slow
+default implementation with a faster less expressive backend.
+
+The default implementation is in :py:mod:`.expressions.sympy`.
+
+There is are wrapper classes for finding non-protocol uses of expression in :py:mod:`.expressions.wrapper`. Define
+``QUPULSE_EXPRESSION_WRAPPER`` environment variable when running python to wrap all expression usages.
+"""
+
+from typing import Type, TypeVar
+from numbers import Real
+import os
+
+import numpy as np
+import sympy as sp
+
+from . import sympy, protocol, wrapper
+
+
+__all__ = ["Expression", "ExpressionVector", "ExpressionScalar",
+ "NonNumericEvaluation", "ExpressionVariableMissingException"]
+
+
+Expression: Type[protocol.Expression] = sympy.Expression
+ExpressionScalar: Type[protocol.ExpressionScalar] = sympy.ExpressionScalar
+ExpressionVector: Type[protocol.ExpressionVector] = sympy.ExpressionVector
+
+
+if os.environ.get('QUPULSE_EXPRESSION_WRAPPER', None): # pragma: no cover
+ Expression, ExpressionScalar, ExpressionVector = wrapper.make_wrappers(sympy.Expression,
+ sympy.ExpressionScalar,
+ sympy.ExpressionVector)
+
+
+ExpressionLike = TypeVar('ExpressionLike', str, Real, sp.Expr, ExpressionScalar)
+
+
+class ExpressionVariableMissingException(Exception):
+ """An exception indicating that a variable value was not provided during expression evaluation.
+
+ See also:
+ qupulse.expressions.Expression
+ """
+
+ def __init__(self, variable: str, expression: Expression) -> None:
+ super().__init__()
+ self.variable = variable
+ self.expression = expression
+
+ def __str__(self) -> str:
+ return f"Could not evaluate <{self.expression}>: A value for variable <{self.variable}> is missing!"
+
+
+class NonNumericEvaluation(Exception):
+ """An exception that is raised if the result of evaluate_numeric is not a number.
+
+ See also:
+ qupulse.expressions.Expression.evaluate_numeric
+ """
+
+ def __init__(self, expression: Expression, non_numeric_result, call_arguments):
+ self.expression = expression
+ self.non_numeric_result = non_numeric_result
+ self.call_arguments = call_arguments
+
+ def __str__(self) -> str:
+ if isinstance(self.non_numeric_result, np.ndarray):
+ dtype = self.non_numeric_result.dtype
+
+ if dtype == np.dtype('O'):
+ dtypes = set(map(type, self.non_numeric_result.flat))
+ return f"The result of evaluate_numeric is an array with the types {dtypes} which is not purely numeric"
+ else:
+ dtype = type(self.non_numeric_result)
+ return f"The result of evaluate_numeric is of type {dtype} which is not a number"
diff --git a/qupulse/expressions/protocol.py b/qupulse/expressions/protocol.py
new file mode 100644
index 000000000..ffe4e6e3b
--- /dev/null
+++ b/qupulse/expressions/protocol.py
@@ -0,0 +1,109 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+"""This module contains the interface / protocol descriptions of ``Expression``, ``ExpressionScalar`` and
+``ExpressionVector``."""
+
+from typing import Mapping, Union, Sequence, Hashable, Any, Protocol
+from numbers import Real
+
+import numpy as np
+
+
+class Ordered(Protocol):
+ def __lt__(self, other):
+ pass
+
+ def __le__(self, other):
+ pass
+
+ def __gt__(self, other):
+ pass
+
+ def __ge__(self, other):
+ pass
+
+
+class Scalar(Protocol):
+ def __add__(self, other):
+ pass
+
+ def __sub__(self, other):
+ pass
+
+ def __mul__(self, other):
+ pass
+
+ def __truediv__(self, other):
+ pass
+
+ def __floordiv__(self, other):
+ pass
+
+ def __ceil__(self):
+ pass
+
+ def __floor__(self):
+ pass
+
+ def __float__(self):
+ pass
+
+ def __int__(self):
+ pass
+
+ def __abs__(self):
+ pass
+
+
+class Expression(Hashable, Protocol):
+ """This protocol defines how Expressions are allowed to be used in qupulse."""
+
+ def evaluate_in_scope(self, scope: Mapping) -> Union[Real, np.ndarray]:
+ """Evaluate the expression by taking the variables from the given scope (typically of type Scope, but it can be
+ any mapping.)
+ Args:
+ scope:
+
+ Returns:
+
+ """
+
+ def evaluate_symbolic(self, substitutions: Mapping[str, Any]) -> 'Expression':
+ """Substitute a part of the expression for another"""
+
+ def evaluate_time_dependent(self, scope: Mapping) -> Union['Expression', Real, np.ndarray]:
+ """Evaluate to a time dependent expression or a constant."""
+
+ @property
+ def variables(self) -> Sequence[str]:
+ """ Get all free variables in the expression.
+
+ Returns:
+ A collection of all free variables occurring in the expression.
+ """
+ raise NotImplementedError()
+
+ @classmethod
+ def make(cls,
+ expression_or_dict,
+ numpy_evaluation=None) -> 'Expression':
+ """Backward compatible expression generation to allow creation from dict."""
+ raise NotImplementedError()
+
+ @property
+ def underlying_expression(self) -> Any:
+ """Return some internal unspecified representation"""
+ raise NotImplementedError()
+
+ def get_serialization_data(self):
+ raise NotImplementedError()
+
+
+class ExpressionScalar(Expression, Scalar, Ordered, Protocol):
+ pass
+
+
+class ExpressionVector(Expression, Protocol):
+ pass
diff --git a/qupulse/expressions.py b/qupulse/expressions/sympy.py
similarity index 86%
rename from qupulse/expressions.py
rename to qupulse/expressions/sympy.py
index bbfad3eed..5f2029a6c 100644
--- a/qupulse/expressions.py
+++ b/qupulse/expressions/sympy.py
@@ -1,9 +1,14 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
"""
This module defines the class Expression to represent mathematical expression as well as
corresponding exception classes.
"""
+import numbers
import operator
-from typing import Any, Dict, Union, Sequence, Callable, TypeVar, Type, Mapping
+from typing import Any, Dict, Union, Sequence, Callable, TypeVar, Type, Mapping, Optional
from numbers import Number
import warnings
import functools
@@ -18,7 +23,9 @@
get_most_simple_representation, get_variables, evaluate_lamdified_exact_rational
from qupulse.utils.types import TimeType
-__all__ = ["Expression", "ExpressionVariableMissingException", "ExpressionScalar", "ExpressionVector", "ExpressionLike"]
+import qupulse.expressions
+
+__all__ = ["Expression", "ExpressionScalar", "ExpressionVector"]
_ExpressionType = TypeVar('_ExpressionType', bound='Expression')
@@ -60,9 +67,9 @@ def _parse_evaluate_numeric_vector(vector_result: numpy.ndarray) -> numpy.ndarra
if not issubclass(vector_result.dtype.type, allowed_scalar):
obj_types = set(map(type, vector_result.flat))
if all(issubclass(obj_type, sympy.Integer) for obj_type in obj_types):
- result = vector_result.astype(numpy.int64)
+ vector_result = vector_result.astype(numpy.int64)
elif all(issubclass(obj_type, (sympy.Integer, sympy.Float)) for obj_type in obj_types):
- result = vector_result.astype(float)
+ vector_result = vector_result.astype(float)
else:
raise ValueError("Could not parse vector result", vector_result)
return vector_result
@@ -98,7 +105,7 @@ def _parse_evaluate_numeric_arguments(self, eval_args: Mapping[str, Number]) ->
# we forward qupulse errors, I down like this
raise
else:
- raise ExpressionVariableMissingException(key_error.args[0], self) from key_error
+ raise qupulse.expressions.ExpressionVariableMissingException(key_error.args[0], self) from key_error
def evaluate_in_scope(self, scope: Mapping) -> Union[Number, numpy.ndarray]:
"""Evaluate the expression by taking the variables from the given scope (typically of type Scope but it can be
@@ -129,7 +136,7 @@ def evaluate_symbolic(self, substitutions: Mapping[Any, Any]) -> 'Expression':
def _evaluate_to_time_dependent(self, scope: Mapping) -> Union['Expression', Number, numpy.ndarray]:
try:
return self.evaluate_numeric(**scope, t=sympy.symbols('t'))
- except NonNumericEvaluation as non_num:
+ except qupulse.expressions.NonNumericEvaluation as non_num:
return ExpressionScalar(non_num.non_numeric_result)
except TypeError:
return self.evaluate_symbolic(scope)
@@ -212,7 +219,7 @@ def evaluate_in_scope(self, scope: Mapping) -> numpy.ndarray:
try:
return _parse_evaluate_numeric_vector(result)
except ValueError as err:
- raise NonNumericEvaluation(self, result, scope) from err
+ raise qupulse.expressions.NonNumericEvaluation(self, result, scope) from err
def get_serialization_data(self) -> Sequence[str]:
serialized_items = list(map(get_most_simple_representation, self._expression_items))
@@ -367,12 +374,28 @@ def __le__(self, other: Union['ExpressionScalar', Number, sympy.Expr]) -> Union[
return None if isinstance(result, sympy.Rel) else bool(result)
def __eq__(self, other: Union['ExpressionScalar', Number, sympy.Expr]) -> bool:
- """Enable comparisons with Numbers"""
+ # the consistency of __hash__ and __eq__ relies on the consistency of the numeric types' behavior.
+ # The types deal with equal floats, integers and rationals for us.
+
+ num_val = self._try_to_numeric()
+ if num_val is not None:
+ return other == num_val
+ elif isinstance(other, ALLOWED_NUMERIC_SCALAR_TYPES):
+ # self is non-numeric but other is
+ # this is a short-cut to avoid an unnecessary sympify call
+ return False
+
+ rhs = self._sympify(other)
+ lhs = self._sympified_expression
+
# sympy's __eq__ checks for structural equality to be consistent regarding __hash__ so we do that too
# see https://github.com/sympy/sympy/issues/18054#issuecomment-566198899
- return self._sympified_expression == self._sympify(other)
+ return lhs == rhs
def __hash__(self) -> int:
+ num_val = self._try_to_numeric()
+ if num_val is not None:
+ return hash(num_val)
return hash(self._sympified_expression)
def __add__(self, other: Union['ExpressionScalar', Number, sympy.Expr]) -> 'ExpressionScalar':
@@ -399,6 +422,12 @@ def __truediv__(self, other: Union['ExpressionScalar', Number, sympy.Expr]) -> '
def __rtruediv__(self, other: Union['ExpressionScalar', Number, sympy.Expr]) -> 'ExpressionScalar':
return self.make(self._sympified_expression.__rtruediv__(self._extract_sympified(other)))
+ def __floordiv__(self, other: Union['ExpressionScalar', Number, sympy.Expr]) -> 'ExpressionScalar':
+ return self.make(self._sympified_expression.__floordiv__(self._extract_sympified(other)))
+
+ def __rfloordiv__(self, other: Union['ExpressionScalar', Number, sympy.Expr]) -> 'ExpressionScalar':
+ return self.make(self._sympified_expression.__rfloordiv__(self._extract_sympified(other)))
+
def __neg__(self) -> 'ExpressionScalar':
return self.make(self._sympified_expression.__neg__())
@@ -408,10 +437,28 @@ def __pos__(self):
def _sympy_(self):
return self._sympified_expression
+ def _try_to_numeric(self) -> Optional[numbers.Number]:
+ """Returns a numeric representation if the expression has no free variables. The difference to __float__ is
+ the proper treatment of integers and rationals."""
+ if self._variables:
+ return None
+ if isinstance(self._original_expression, ALLOWED_NUMERIC_SCALAR_TYPES):
+ return self._original_expression
+ expr = self._sympified_expression.doit()
+ if isinstance(expr, bool):
+ # sympify can return bool
+ return expr
+ if expr.is_Float:
+ return float(expr)
+ if expr.is_Integer:
+ return int(expr)
+ else:
+ return TimeType.from_sympy(expr)
+
@property
def original_expression(self) -> Union[str, Number]:
if self._original_expression is None:
- return str(self._sympified_expression)
+ return get_most_simple_representation(self._sympified_expression)
else:
return self._original_expression
@@ -444,7 +491,7 @@ def evaluate_with_exact_rationals(self, scope: Mapping) -> Union[Number, numpy.n
try:
return _parse_evaluate_numeric(result)
except ValueError as err:
- raise NonNumericEvaluation(self, result, scope) from err
+ raise qupulse.expressions.NonNumericEvaluation(self, result, scope) from err
def evaluate_in_scope(self, scope: Mapping) -> Union[Number, numpy.ndarray]:
parsed_kwargs = self._parse_evaluate_numeric_arguments(scope)
@@ -453,50 +500,4 @@ def evaluate_in_scope(self, scope: Mapping) -> Union[Number, numpy.ndarray]:
try:
return _parse_evaluate_numeric(result)
except ValueError as err:
- raise NonNumericEvaluation(self, result, scope) from err
-
-
-class ExpressionVariableMissingException(Exception):
- """An exception indicating that a variable value was not provided during expression evaluation.
-
- See also:
- qupulse.expressions.Expression
- """
-
- def __init__(self, variable: str, expression: Expression) -> None:
- super().__init__()
- self.variable = variable
- self.expression = expression
-
- def __str__(self) -> str:
- return "Could not evaluate <{}>: A value for variable <{}> is missing!".format(
- str(self.expression), self.variable)
-
-
-class NonNumericEvaluation(Exception):
- """An exception that is raised if the result of evaluate_numeric is not a number.
-
- See also:
- qupulse.expressions.Expression.evaluate_numeric
- """
-
- def __init__(self, expression: Expression, non_numeric_result: Any, call_arguments: Mapping):
- self.expression = expression
- self.non_numeric_result = non_numeric_result
- self.call_arguments = call_arguments
-
- def __str__(self) -> str:
- if isinstance(self.non_numeric_result, numpy.ndarray):
- dtype = self.non_numeric_result.dtype
-
- if dtype == numpy.dtype('O'):
- dtypes = set(map(type, self.non_numeric_result.flat))
- "The result of evaluate_numeric is an array with the types {} " \
- "which is not purely numeric".format(dtypes)
- else:
- dtype = type(self.non_numeric_result)
- return "The result of evaluate_numeric is of type {} " \
- "which is not a number".format(dtype)
-
-
-ExpressionLike = TypeVar('ExpressionLike', str, Number, sympy.Expr, ExpressionScalar)
+ raise qupulse.expressions.NonNumericEvaluation(self, result, scope) from err
diff --git a/qupulse/expressions/wrapper.py b/qupulse/expressions/wrapper.py
new file mode 100644
index 000000000..c13e913f5
--- /dev/null
+++ b/qupulse/expressions/wrapper.py
@@ -0,0 +1,121 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+"""This module contains the function :py:``make_wrappers`` to define wrapper classes for expression protocol implementations
+which only implements methods of the protocol.
+It is used for finding code that relies on expression implementation details."""
+
+import math
+from typing import Sequence, Any, Mapping, Union, Tuple
+from numbers import Real
+
+import numpy as np
+
+from qupulse.expressions import protocol, sympy
+
+
+def make_wrappers(expr: type, expr_scalar: type, expr_vector: type) -> Tuple[type, type, type]:
+ """Create wrappers for expression base, scalar and vector types that only expose the methods defined in the
+ corresponding expression protocol classes.
+
+ The vector is currently not implemented.
+
+ Args:
+ expr: Expression base type of the implementation
+ expr_scalar: Expression scalar type of the implementation
+ expr_vector: Expression vector type of the implementation
+
+ Returns:
+ A tuple of (base, scalar, vector) types that wrap the given types.
+ """
+
+ class ExpressionWrapper(protocol.Expression):
+ def __init__(self, x):
+ self._wrapped: protocol.Expression = expr(x)
+
+ @classmethod
+ def make(cls, expression_or_dict, numpy_evaluation=None) -> 'ExpressionWrapper':
+ return cls(expression_or_dict)
+
+ @property
+ def underlying_expression(self) -> Any:
+ return self._wrapped.underlying_expression
+
+ def __hash__(self) -> int:
+ return hash(self._wrapped)
+
+ def __eq__(self, other):
+ return self._wrapped == getattr(other, '_wrapped', other)
+
+ @property
+ def variables(self) -> Sequence[str]:
+ return self._wrapped.variables
+
+ def evaluate_in_scope(self, scope: Mapping) -> Union[Real, np.ndarray]:
+ return self._wrapped.evaluate_in_scope(scope)
+
+ def evaluate_symbolic(self, substitutions: Mapping[str, Any]) -> 'ExpressionWrapper':
+ """Substitute a part of the expression for another"""
+ return ExpressionWrapper(self._wrapped.evaluate_symbolic(substitutions))
+
+ def evaluate_time_dependent(self, scope: Mapping) -> Union['Expression', Real, np.ndarray]:
+ """Evaluate to a time dependent expression or a constant."""
+ return self._wrapped.evaluate_time_dependent(scope)
+
+ def get_serialization_data(self):
+ return self._wrapped.get_serialization_data()
+
+ class ExpressionScalarWrapper(ExpressionWrapper, protocol.ExpressionScalar):
+ def __init__(self, x):
+ ExpressionWrapper.__init__(self, 0)
+ self._wrapped: protocol.ExpressionScalar = expr_scalar(x)
+
+ # Scalar
+ def __add__(self, other):
+ return ExpressionScalarWrapper(self._wrapped + getattr(other, '_wrapped', other))
+
+ def __sub__(self, other):
+ return ExpressionScalarWrapper(self._wrapped - getattr(other, '_wrapped', other))
+
+ def __mul__(self, other):
+ return ExpressionScalarWrapper(self._wrapped * getattr(other, '_wrapped', other))
+
+ def __truediv__(self, other):
+ return ExpressionScalarWrapper(self._wrapped / getattr(other, '_wrapped', other))
+
+ def __floordiv__(self, other):
+ return ExpressionScalarWrapper(self._wrapped // getattr(other, '_wrapped', other))
+
+ def __ceil__(self):
+ return ExpressionScalarWrapper(math.ceil(self._wrapped))
+
+ def __floor__(self):
+ return ExpressionScalarWrapper(math.floor(self._wrapped))
+
+ def __float__(self):
+ return float(self._wrapped)
+
+ def __int__(self):
+ return int(self._wrapped)
+
+ def __abs__(self):
+ return ExpressionScalarWrapper(abs(self._wrapped))
+
+ # Ordered
+ def __lt__(self, other):
+ return self._wrapped < getattr(other, '_wrapped', other)
+
+ def __le__(self, other):
+ return self._wrapped <= getattr(other, '_wrapped', other)
+
+ def __gt__(self, other):
+ return self._wrapped > getattr(other, '_wrapped', other)
+
+ def __ge__(self, other):
+ return self._wrapped >= getattr(other, '_wrapped', other)
+
+ class ExpressionVectorWrapper(ExpressionWrapper):
+ pass
+
+ return ExpressionWrapper, ExpressionScalarWrapper, ExpressionVectorWrapper
diff --git a/qupulse/hardware/__init__.py b/qupulse/hardware/__init__.py
index a545e01d8..e524f3d9a 100644
--- a/qupulse/hardware/__init__.py
+++ b/qupulse/hardware/__init__.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
"""Contains drivers for AWG control and digitizer configuration as well as a unifying interface to all instruments:
:class:`~qupulse.hardware.setup.HardwareSetup`"""
diff --git a/qupulse/hardware/awgs/__init__.py b/qupulse/hardware/awgs/__init__.py
index d91e1833d..d7d629a4d 100644
--- a/qupulse/hardware/awgs/__init__.py
+++ b/qupulse/hardware/awgs/__init__.py
@@ -1,29 +1,16 @@
-import sys
-import subprocess
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
-__all__ = ["install_requirements"]
+import lazy_loader as lazy
-try:
- from qupulse.hardware.awgs.tabor import TaborAWGRepresentation, TaborChannelPair
- __all__.extend(["TaborAWGRepresentation", "TaborChannelPair"])
-except ImportError:
- pass
-try:
- from qupulse.hardware.awgs.tektronix import TektronixAWG
- __all__.extend(["TektronixAWG"])
-except ImportError:
- pass
-
-
-def install_requirements(vendor: str):
- package_repos = {
- 'tektronix': 'tek_awg',
- 'tabor': 'tabor_control'
+__getattr__, __dir__, __all__ = lazy.attach(
+ __name__,
+ submodules={'base'},
+ submod_attrs={
+ 'tabor': ['TaborAWGRepresentation', 'TaborChannelPair'],
+ 'tektronix': ['TektronixAWG'],
+ 'zihdawg': ['HDAWGRepresentation', 'HDAWGChannelGroup'],
}
-
- if vendor not in package_repos:
- raise ValueError('Vendor must be in {}'.format(set(package_repos.keys())))
-
- repo = package_repos[vendor]
- subprocess.check_call([sys.executable, "-m", "pip", "install", repo])
+)
diff --git a/qupulse/hardware/awgs/base.py b/qupulse/hardware/awgs/base.py
index 498788038..2a62fdc35 100644
--- a/qupulse/hardware/awgs/base.py
+++ b/qupulse/hardware/awgs/base.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
"""This module defines the common interface for arbitrary waveform generators.
Classes:
@@ -9,14 +13,17 @@
from abc import abstractmethod
from numbers import Real
-from typing import Set, Tuple, Callable, Optional, Mapping, Sequence, List
+from typing import Set, Tuple, Callable, Optional, Mapping, Sequence, List, Union, NamedTuple
from collections import OrderedDict
+from enum import Enum
+import warnings
from qupulse.hardware.util import get_sample_times, not_none_indices
from qupulse.utils.types import ChannelID
-from qupulse._program._loop import Loop
-from qupulse._program.waveforms import Waveform
-from qupulse.comparable import Comparable
+from qupulse.program.linspace import LinSpaceNode, LinSpaceProgram, Play, \
+ transform_linspace_commands, to_increment_commands
+from qupulse.program.loop import Loop
+from qupulse.program.waveforms import Waveform
from qupulse.utils.types import TimeType
import numpy
@@ -35,7 +42,7 @@ class AWGAmplitudeOffsetHandling:
_valid = [IGNORE_OFFSET, CONSIDER_OFFSET]
-class AWG(Comparable):
+class AWG:
"""An arbitrary waveform generator abstraction class.
It represents a set of channels that have to have(hardware enforced) the same:
@@ -138,6 +145,14 @@ def sample_rate(self) -> float:
def compare_key(self) -> int:
"""Comparison and hashing is based on the id of the AWG so different devices with the same properties
are ot equal"""
+ warnings.warn("AWG.compare_key is deprecated since 0.11 and will be removed in 0.12",
+ DeprecationWarning, stacklevel=2)
+ return id(self)
+
+ def __eq__(self, other):
+ return self is other
+
+ def __hash__(self):
return id(self)
@abstractmethod
@@ -162,17 +177,28 @@ def __str__(self) -> str:
" Use force to overwrite.".format(self.name)
+#!!! typehint obsolete
+AllowedProgramTypes = Union[Loop,LinSpaceProgram,]
+
+
+class ChannelTransformation(NamedTuple):
+ amplitude: float
+ offset: float
+ voltage_transformation: Optional[callable]
+
+
class ProgramEntry:
"""This is a helper class for implementing awgs drivers. A driver can subclass it to help organizing sampled
waveforms"""
- def __init__(self, loop: Loop,
+ def __init__(self, program: AllowedProgramTypes,
channels: Tuple[Optional[ChannelID], ...],
markers: Tuple[Optional[ChannelID], ...],
amplitudes: Tuple[float, ...],
offsets: Tuple[float, ...],
voltage_transformations: Tuple[Optional[Callable], ...],
sample_rate: TimeType,
- waveforms: Sequence[Waveform] = None):
+ waveforms: Sequence[Waveform] = None,
+ ):
"""
Args:
@@ -195,17 +221,27 @@ def __init__(self, loop: Loop,
self._voltage_transformations = tuple(voltage_transformations)
self._sample_rate = sample_rate
-
- self._loop = loop
-
+
+ self._program = program
+
if waveforms is None:
- waveforms = OrderedDict((node.waveform, None)
- for node in loop.get_depth_first_iterator() if node.is_leaf()).keys()
+ #!!! this formulation is also unfortunate as the channel transformations are
+ # not applied to the waveforms but only to 'Set'/'Increment'-like commands in LSB
+ waveforms_dict = program.get_waveforms_dict(channels, self._channel_transformations())
+ waveforms = waveforms_dict.keys()
if waveforms:
self._waveforms = OrderedDict(zip(waveforms, self._sample_waveforms(waveforms)))
else:
self._waveforms = OrderedDict()
-
+
+ @property
+ def _loop(self,) -> AllowedProgramTypes:
+ return self._program
+
+ @_loop.setter
+ def _loop(self, program: AllowedProgramTypes):
+ self._program = program
+
def _sample_empty_channel(self, time: numpy.ndarray) -> Optional[numpy.ndarray]:
"""Override this in derived class to change how empty channels are handled"""
return None
@@ -214,6 +250,13 @@ def _sample_empty_marker(self, time: numpy.ndarray) -> Optional[numpy.ndarray]:
"""Override this in derived class to change how empty channels are handled"""
return None
+ def _channel_transformations(self) -> Mapping[ChannelID, ChannelTransformation]:
+ return {ch: ChannelTransformation(amplitude, offset, trafo)
+ for ch, trafo, amplitude, offset in zip(self._channels,
+ self._voltage_transformations,
+ self._amplitudes,
+ self._offsets)}
+
def _sample_waveforms(self, waveforms: Sequence[Waveform]) -> List[Tuple[Tuple[numpy.ndarray, ...],
Tuple[numpy.ndarray, ...]]]:
sampled_waveforms = []
diff --git a/qupulse/hardware/awgs/dummy.py b/qupulse/hardware/awgs/dummy.py
new file mode 100644
index 000000000..1e2862a32
--- /dev/null
+++ b/qupulse/hardware/awgs/dummy.py
@@ -0,0 +1,77 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+from typing import Tuple, Set
+
+from .base import AWG, ProgramOverwriteException
+
+class DummyAWG(AWG):
+ def __init__(self,
+ sample_rate: float=10,
+ output_range: Tuple[float, float]=(-5, 5),
+ num_channels: int=1,
+ num_markers: int=1) -> None:
+ """Dummy AWG for automated testing, debugging and usage in examples.
+
+ Args:
+ sample_rate (float): The sample rate of the dummy. (default = 10)
+ output_range (float, float): A (min,max)-tuple of possible output values.
+ (default = (-5,5)).
+ """
+ super().__init__(identifier="DummyAWG{0}".format(id(self)))
+
+ self._programs = {} # contains program names and programs
+ self._sample_rate = sample_rate
+ self._output_range = output_range
+ self._num_channels = num_channels
+ self._num_markers = num_markers
+ self._channels = ('default',)
+ self._armed = None
+
+ def set_volatile_parameters(self, program_name: str, parameters):
+ raise NotImplementedError()
+
+ def upload(self, name, program, channels, markers, voltage_transformation, force=False) -> None:
+ if name in self.programs:
+ if not force:
+ raise ProgramOverwriteException(name)
+ else:
+ self.remove(name)
+ self.upload(name, program, channels, markers, voltage_transformation)
+ else:
+ self._programs[name] = (program, channels, markers, voltage_transformation)
+
+ def remove(self, name) -> None:
+ if name in self.programs:
+ self._programs.pop(name)
+
+ def clear(self) -> None:
+ self._programs = {}
+
+ def arm(self, name: str) -> None:
+ self._armed = name
+
+ @property
+ def programs(self) -> Set[str]:
+ return set(self._programs.keys())
+
+ @property
+ def output_range(self) -> Tuple[float, float]:
+ return self._output_range
+
+ @property
+ def identifier(self) -> str:
+ return "DummyAWG{0}".format(id(self))
+
+ @property
+ def sample_rate(self) -> float:
+ return self._sample_rate
+
+ @property
+ def num_channels(self):
+ return self._num_channels
+
+ @property
+ def num_markers(self):
+ return self._num_markers
diff --git a/qupulse/hardware/awgs/tabor.py b/qupulse/hardware/awgs/tabor.py
index 6e661bc0c..59385d3d9 100644
--- a/qupulse/hardware/awgs/tabor.py
+++ b/qupulse/hardware/awgs/tabor.py
@@ -1,6 +1,8 @@
-import fractions
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
import functools
-import warnings
import weakref
import logging
import numbers
@@ -10,12 +12,12 @@
import tabor_control.device
import numpy as np
-from qupulse.utils.types import ChannelID
-from qupulse._program._loop import Loop, make_compatible
-from qupulse.hardware.util import voltage_to_uint16, find_positions, traced
+from qupulse.utils.types import ChannelID, TimeType
+from qupulse.program.loop import Loop, make_compatible
+from qupulse.hardware.util import voltage_to_uint16, traced
from qupulse.hardware.awgs.base import AWG, AWGAmplitudeOffsetHandling
from qupulse._program.tabor import TaborSegment, TaborException, TaborProgram, PlottableProgram, TaborSequencing,\
- make_combined_wave
+ make_combined_wave, find_place_for_segments_in_memory
__all__ = ['TaborAWGRepresentation', 'TaborChannelPair']
@@ -466,7 +468,7 @@ def upload(self, name: str,
make_compatible(program,
minimal_waveform_length=192,
waveform_quantum=16,
- sample_rate=fractions.Fraction(sample_rate, 10**9))
+ sample_rate=TimeType.from_fraction(sample_rate, 10**9))
if name in self._known_programs:
if force:
@@ -543,87 +545,16 @@ def clear(self) -> None:
self.change_armed_program(None)
def _find_place_for_segments_in_memory(self, segments: Sequence, segment_lengths: Sequence) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
- """
- 1. Find known segments
- 2. Find empty spaces with fitting length
- 3. Find empty spaces with bigger length
- 4. Amend remaining segments
- :param segments:
- :param segment_lengths:
- :return:
- """
- segment_hashes = np.fromiter((hash(segment) for segment in segments), count=len(segments), dtype=np.int64)
-
- waveform_to_segment = find_positions(self._segment_hashes, segment_hashes)
-
- # separate into known and unknown
- unknown = (waveform_to_segment == -1)
- known = ~unknown
-
- known_pos_in_memory = waveform_to_segment[known]
-
- assert len(known_pos_in_memory) == 0 or np.all(self._segment_hashes[known_pos_in_memory] == segment_hashes[known])
-
- new_reference_counter = self._segment_references.copy()
- new_reference_counter[known_pos_in_memory] += 1
-
- to_upload_size = np.sum(segment_lengths[unknown] + 16)
- free_points_in_total = self.total_capacity - np.sum(self._segment_capacity[self._segment_references > 0])
- if free_points_in_total < to_upload_size:
- raise MemoryError('Not enough free memory',
- free_points_in_total,
- to_upload_size,
- self._free_points_in_total)
-
- to_amend = cast(np.ndarray, unknown)
- to_insert = np.full(len(segments), fill_value=-1, dtype=np.int64)
+ segment_hashes = np.fromiter((hash(segment) for segment in segments), dtype=np.int64, count=len(segments))
- reserved_indices = np.flatnonzero(new_reference_counter > 0)
- first_free = reserved_indices[-1] + 1 if len(reserved_indices) else 0
-
- free_segments = new_reference_counter[:first_free] == 0
- free_segment_count = np.sum(free_segments)
-
- # look for a free segment place with the same length
- for segment_idx in np.flatnonzero(to_amend):
- if free_segment_count == 0:
- break
-
- pos_of_same_length = np.logical_and(free_segments, segment_lengths[segment_idx] == self._segment_capacity[:first_free])
- idx_same_length = np.argmax(pos_of_same_length)
- if pos_of_same_length[idx_same_length]:
- free_segments[idx_same_length] = False
- free_segment_count -= 1
-
- to_amend[segment_idx] = False
- to_insert[segment_idx] = idx_same_length
-
- # try to find places that are larger than the segments to fit in starting with the large segments and large
- # free spaces
- segment_indices = np.flatnonzero(to_amend)[np.argsort(segment_lengths[to_amend])[::-1]]
- capacities = self._segment_capacity[:first_free]
- for segment_idx in segment_indices:
- free_capacities = capacities[free_segments]
- free_segments_indices = np.flatnonzero(free_segments)[np.argsort(free_capacities)[::-1]]
-
- if len(free_segments_indices) == 0:
- break
-
- fitting_segment = np.argmax((free_capacities >= segment_lengths[segment_idx])[::-1])
- fitting_segment = free_segments_indices[fitting_segment]
- if self._segment_capacity[fitting_segment] >= segment_lengths[segment_idx]:
- free_segments[fitting_segment] = False
- to_amend[segment_idx] = False
- to_insert[segment_idx] = fitting_segment
-
- free_points_at_end = self.total_capacity - np.sum(self._segment_capacity[:first_free])
- if np.sum(segment_lengths[to_amend] + 16) > free_points_at_end:
- raise MemoryError('Fragmentation does not allow upload.',
- np.sum(segment_lengths[to_amend] + 16),
- free_points_at_end,
- self._free_points_at_end)
-
- return waveform_to_segment, to_amend, to_insert
+ return find_place_for_segments_in_memory(
+ current_segment_hashes=self._segment_hashes,
+ current_segment_capacities=self._segment_capacity,
+ current_segment_references=self._segment_references,
+ total_capacity=self.total_capacity,
+ new_segment_lengths=segment_lengths,
+ new_segment_hashes=segment_hashes
+ )
@with_select
@with_configuration_guard
@@ -953,3 +884,4 @@ def reset_device(self):
self.device.reset()
elif isinstance(self.device, TaborChannelPair):
self.device.clear()
+
diff --git a/qupulse/hardware/awgs/tektronix.py b/qupulse/hardware/awgs/tektronix.py
index 801cf2eed..445ea1eef 100644
--- a/qupulse/hardware/awgs/tektronix.py
+++ b/qupulse/hardware/awgs/tektronix.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
from typing import Tuple, Callable, Optional, Sequence, Union, Dict, Mapping, Set
from types import MappingProxyType
import numpy as np
@@ -7,17 +11,12 @@
import warnings
import logging
-try:
- import tek_awg
-except ImportError: # pragma: no cover
- warnings.warn("Could not import Tektronix driver backend. "
- "If you wish to use it execute qupulse.hardware.awgs.install_requirements('tektronix')")
- raise
+import tek_awg
from qupulse.hardware.awgs.base import AWG, AWGAmplitudeOffsetHandling, ProgramOverwriteException
from qupulse import ChannelID
-from qupulse._program._loop import Loop, make_compatible
-from qupulse._program.waveforms import Waveform as QuPulseWaveform
+from qupulse.program.loop import Loop, make_compatible
+from qupulse.program.waveforms import Waveform as QuPulseWaveform
from qupulse.utils.types import TimeType
from qupulse.hardware.util import voltage_to_uint16, get_sample_times, traced
from qupulse.utils import pairwise
@@ -286,9 +285,6 @@ def __init__(self, device: tek_awg.TekAwg,
super().__init__(identifier=identifier)
self.logger = logger or logging.getLogger("qupulse.tektronix")
- if device is None:
- raise RuntimeError('Please install the tek_awg package or run "install_requirements" from this module')
-
self._device = device
self._synchronized = False # this gets set to True by synchronize or clear and to False on error during manupulation
diff --git a/qupulse/hardware/awgs/zihdawg.py b/qupulse/hardware/awgs/zihdawg.py
index 6d6972a90..a466e29ea 100644
--- a/qupulse/hardware/awgs/zihdawg.py
+++ b/qupulse/hardware/awgs/zihdawg.py
@@ -1,1133 +1,25 @@
-import numbers
-from pathlib import Path
-import functools
-from typing import Tuple, Set, Callable, Optional, Mapping, Generator, Union, Sequence, Dict
-from enum import Enum
-import weakref
-import logging
-import warnings
-import pathlib
-import hashlib
-import argparse
-import re
-from abc import abstractmethod
-
-try:
- # zhinst fires a DeprecationWarning from its own code in some versions...
- with warnings.catch_warnings():
- warnings.simplefilter('ignore', DeprecationWarning)
- import zhinst.utils
-except ImportError:
- warnings.warn('Zurich Instruments LabOne python API is distributed via the Python Package Index. Install with pip.')
- raise
-
-try:
- from zhinst import core as zhinst_core
-except ImportError:
- # backward compability
- from zhinst import ziPython as zhinst_core
-
-import time
-
-from qupulse.utils.types import ChannelID, TimeType, time_from_float
-from qupulse._program._loop import Loop, make_compatible
-from qupulse._program.seqc import HDAWGProgramManager, UserRegister, WaveformFileSystem
-from qupulse.hardware.awgs.base import AWG, ChannelNotFoundException, AWGAmplitudeOffsetHandling
-from qupulse.hardware.util import traced
-
-
-logger = logging.getLogger('qupulse.hdawg')
-
-
-def valid_channel(function_object):
- """Check if channel is a valid AWG channels. Expects channel to be 2nd argument after self."""
- @functools.wraps(function_object)
- def valid_fn(*args, **kwargs):
- if len(args) < 2:
- raise HDAWGTypeError('Channel is an required argument.')
- channel = args[1] # Expect channel to be second positional argument after self.
- if channel not in range(1, 9):
- raise ChannelNotFoundException(channel)
- value = function_object(*args, **kwargs)
- return value
- return valid_fn
-
-
-def _amplitude_scales(api_session, serial: str):
- return tuple(
- api_session.getDouble(f'/{serial}/awgs/{ch // 2:d}/outputs/{ch % 2:d}/amplitude')
- for ch in range(8)
- )
-
-def _sigout_double(api_session, prop: str, serial: str, channel: int, value: float = None) -> float:
- """Query channel offset voltage and optionally set it."""
- node_path = f'/{serial}/sigouts/{channel-1:d}/{prop}'
- if value is not None:
- api_session.setDouble(node_path, value)
- api_session.sync() # Global sync: Ensure settings have taken effect on the device.
- return api_session.getDouble(node_path)
-
-def _sigout_range(api_session, serial: str, channel: int, voltage: float = None) -> float:
- return _sigout_double(api_session, 'range', serial, channel, voltage)
-
-def _sigout_offset(api_session, serial: str, channel: int, voltage: float = None) -> float:
- return _sigout_double(api_session, 'offset', serial, channel, voltage)
-
-def _sigout_on(api_session, serial: str, channel: int, value: bool = None) -> bool:
- """Query channel signal output status (enabled/disabled) and optionally set it. Corresponds to front LED."""
- node_path = f'/{serial}/sigouts/{channel-1:d}/on'
- if value is not None:
- api_session.setInt(node_path, value)
- api_session.sync() # Global sync: Ensure settings have taken effect on the device.
- return bool(api_session.getInt(node_path))
-
-
-@traced
-class HDAWGRepresentation:
- """HDAWGRepresentation represents an HDAWG8 instruments and manages a LabOne data server api session. A data server
- must be running and the device be discoverable. Channels are per default grouped into pairs."""
-
- def __init__(self, device_serial: str = None,
- device_interface: str = '1GbE',
- data_server_addr: str = 'localhost',
- data_server_port: int = 8004,
- api_level_number: int = 6,
- reset: bool = False,
- timeout: float = 20,
- grouping: 'HDAWGChannelGrouping' = None) -> None:
- """
- :param device_serial: Device serial that uniquely identifies this device to the LabOne data server
- :param device_interface: Either '1GbE' for ethernet or 'USB'
- :param data_server_addr: Data server address. Must be already running. Default: localhost
- :param data_server_port: Data server port. Default: 8004 for HDAWG, MF and UHF devices
- :param api_level_number: Version of API to use for the session, higher number, newer. Default: 6 most recent
- :param reset: Reset device before initialization
- :param timeout: Timeout in seconds for uploading
- """
- self._api_session = zhinst_core.ziDAQServer(data_server_addr, data_server_port, api_level_number)
- assert zhinst.utils.api_server_version_check(self.api_session) # Check equal data server and api version.
- self.api_session.connectDevice(device_serial, device_interface)
- self.default_timeout = timeout
- self._dev_ser = device_serial
-
- if reset:
- # Create a base configuration: Disable all available outputs, awgs, demods, scopes,...
- zhinst.utils.disable_everything(self.api_session, self.serial)
-
- self._initialize()
-
- waveform_path = pathlib.Path(self.api_session.awgModule().getString('directory'), 'awg', 'waves')
- self._waveform_file_system = WaveformFileSystem.get_waveform_file_system(waveform_path)
- self._channel_groups: Dict[HDAWGChannelGrouping, Tuple[HDAWGChannelGroup, ...]] = {}
-
- # TODO: lookup method to find channel count
- n_channels = 8
-
- for grouping in HDAWGChannelGrouping:
- group_size = grouping.group_size()
- if group_size is None:
- # MDS
- groups = [
- MDSChannelGroup(self.group_name(0, None), self.default_timeout)
- ]
- else:
- groups = []
- for group_idx in range(n_channels // group_size):
- groups.append(SingleDeviceChannelGroup(group_idx, group_size,
- identifier=self.group_name(group_idx, group_size),
- timeout=self.default_timeout))
- self._channel_groups[grouping] = tuple(groups)
-
- if grouping is None:
- grouping = self.channel_grouping
- # activates channel groups
- self.channel_grouping = grouping
-
- @property
- def waveform_file_system(self) -> WaveformFileSystem:
- return self._waveform_file_system
-
- @property
- def channel_tuples(self) -> Tuple['HDAWGChannelGroup', ...]:
- return self._get_groups(self.channel_grouping)
-
- @property
- def channel_pair_AB(self) -> 'HDAWGChannelGroup':
- return self._channel_groups[HDAWGChannelGrouping.CHAN_GROUP_4x2][0]
-
- @property
- def channel_pair_CD(self) -> 'HDAWGChannelGroup':
- return self._channel_groups[HDAWGChannelGrouping.CHAN_GROUP_4x2][1]
-
- @property
- def channel_pair_EF(self) -> 'HDAWGChannelGroup':
- return self._channel_groups[HDAWGChannelGrouping.CHAN_GROUP_4x2][2]
-
- @property
- def channel_pair_GH(self) -> 'HDAWGChannelGroup':
- return self._channel_groups[HDAWGChannelGrouping.CHAN_GROUP_4x2][3]
-
- @property
- def api_session(self) -> zhinst_core.ziDAQServer:
- return self._api_session
-
- @property
- def serial(self) -> str:
- return self._dev_ser
-
- def _initialize(self) -> None:
- settings = [(f'/{self.serial}/awgs/*/userregs/*', 0), # Reset all user registers to 0.
- (f'/{self.serial}/*/single', 1)] # Single execution mode of sequence.
- for ch in range(0, 8): # Route marker 1 signal for each channel to marker output.
- if ch % 2 == 0:
- output = HDAWGTriggerOutSource.OUT_1_MARK_1.value
- else:
- output = HDAWGTriggerOutSource.OUT_1_MARK_2.value
- settings.append(['/{}/triggers/out/{}/source'.format(self.serial, ch), output])
-
- self.api_session.set(settings)
- self.api_session.sync() # Global sync: Ensure settings have taken effect on the device.
-
- def reset(self) -> None:
- zhinst.utils.disable_everything(self.api_session, self.serial)
- self._initialize()
- for tuple in self.channel_tuples:
- tuple.clear()
- self.api_session.set([
- (f'/{self.serial}/awgs/*/time', 0),
- (f'/{self.serial}/sigouts/*/range', HDAWGVoltageRange.RNG_1V.value),
- (f'/{self.serial}/awgs/*/outputs/*/amplitude', 1.0),
- (f'/{self.serial}/outputs/*/modulation/mode', HDAWGModulationMode.OFF.value),
- ])
-
- # marker outputs
- marker_settings = []
- for ch in range(0, 8): # Route marker 1 signal for each channel to marker output.
- if ch % 2 == 0:
- output = HDAWGTriggerOutSource.OUT_1_MARK_1.value
- else:
- output = HDAWGTriggerOutSource.OUT_1_MARK_2.value
- marker_settings.append([f'/{self.serial}/triggers/out/{ch}/source', output])
- self.api_session.set(marker_settings)
- self.api_session.sync()
-
- def group_name(self, group_idx, group_size) -> str:
- if group_size is None:
- return f'{self.serial}_MDS'
- return str(self.serial) + '_' + 'ABCDEFGH'[group_idx*group_size:][:group_size]
-
- def _get_groups(self, grouping: 'HDAWGChannelGrouping') -> Tuple['HDAWGChannelGroup', ...]:
- try:
- return self._channel_groups[grouping]
- except KeyError:
- # python reload...
- for grouping_key, group in self._channel_groups.items():
- if grouping_key.value == grouping.value:
- return group
- else:
- raise
-
- @property
- def channel_grouping(self) -> 'HDAWGChannelGrouping':
- grouping = self.api_session.getInt(f'/{self.serial}/SYSTEM/AWG/CHANNELGROUPING')
- return HDAWGChannelGrouping(grouping)
-
- @channel_grouping.setter
- def channel_grouping(self, channel_grouping: 'HDAWGChannelGrouping'):
- # ipython reload ...
- if not type(channel_grouping).__name__ == 'HDAWGChannelGrouping':
- raise HDAWGTypeError('Channel grouping must be an enum of type "HDAWGChannelGrouping" to avoid confusions '
- 'between enum value and group size.')
- old_channel_grouping = self.channel_grouping
- if old_channel_grouping != channel_grouping:
- self.api_session.setInt(f'/{self.serial}/AWGS/*/ENABLE', 0)
- self.api_session.setInt(f'/{self.serial}/SYSTEM/AWG/CHANNELGROUPING', channel_grouping.value)
- # disable old groups
- for group in self._get_groups(old_channel_grouping):
- group.disconnect_group()
-
- if channel_grouping.value == HDAWGChannelGrouping.MDS.value and not self._is_mds_master():
- # do not connect channel group
- return
-
- for group in self._get_groups(channel_grouping):
- if not group.is_connected():
- group.connect_group(self)
-
- @valid_channel
- def offset(self, channel: int, voltage: float = None) -> float:
- """Query channel offset voltage and optionally set it."""
- return _sigout_offset(self.api_session, self.serial, channel, voltage)
-
- @valid_channel
- def range(self, channel: int, voltage: float = None) -> float:
- """Query channel voltage range and optionally set it. The instruments selects the next higher available range.
- This is the one-sided range Vp. Total range: -Vp...Vp"""
- return _sigout_range(self.api_session, self.serial, channel, voltage)
-
- @valid_channel
- def output(self, channel: int, status: bool = None) -> bool:
- """Query channel signal output status (enabled/disabled) and optionally set it. Corresponds to front LED."""
- return _sigout_on(self.api_session, self.serial, channel, status)
-
- def get_status_table(self):
- """Return node tree of instrument with all important settings, as well as each channel group as tuple."""
- return (self.api_session.get('/{}/*'.format(self.serial)),
- self.channel_pair_AB.awg_module.get('awgModule/*'),
- self.channel_pair_CD.awg_module.get('awgModule/*'),
- self.channel_pair_EF.awg_module.get('awgModule/*'),
- self.channel_pair_GH.awg_module.get('awgModule/*'))
-
- def _get_mds_group_idx(self) -> Optional[int]:
- idx = 0
- while True:
- try:
- if self.serial in self.api_session.getString(f'/ZI/MDS/GROUPS/{idx}/DEVICES'):
- return idx
- except RuntimeError:
- break
- idx += 1
-
- def _is_mds_master(self) -> Optional[bool]:
- idx = 0
- while True:
- try:
- devices = self.api_session.getString(f'/ZI/MDS/GROUPS/{idx}/DEVICES').split(',')
- except RuntimeError:
- break
-
- if self.serial in devices:
- return devices[0] == self.serial
- idx += 1
-
- def __repr__(self):
- return f"{type(self).__name__}({self.serial}, ... {self.api_session})"
-
-
-class HDAWGTriggerOutSource(Enum):
- """Assign a signal to a marker output. This is per AWG Core."""
- AWG_TRIG_1 = 0 # Trigger output assigned to AWG trigger 1, controlled by AWG sequencer commands.
- AWG_TRIG_2 = 1 # Trigger output assigned to AWG trigger 2, controlled by AWG sequencer commands.
- AWG_TRIG_3 = 2 # Trigger output assigned to AWG trigger 3, controlled by AWG sequencer commands.
- AWG_TRIG_4 = 3 # Trigger output assigned to AWG trigger 4, controlled by AWG sequencer commands.
- OUT_1_MARK_1 = 4 # Trigger output assigned to output 1 marker 1.
- OUT_1_MARK_2 = 5 # Trigger output assigned to output 1 marker 2.
- OUT_2_MARK_1 = 6 # Trigger output assigned to output 2 marker 1.
- OUT_2_MARK_2 = 7 # Trigger output assigned to output 2 marker 2.
- TRIG_IN_1 = 8 # Trigger output assigned to trigger inout 1.
- TRIG_IN_2 = 9 # Trigger output assigned to trigger inout 2.
- TRIG_IN_3 = 10 # Trigger output assigned to trigger inout 3.
- TRIG_IN_4 = 11 # Trigger output assigned to trigger inout 4.
- TRIG_IN_5 = 12 # Trigger output assigned to trigger inout 5.
- TRIG_IN_6 = 13 # Trigger output assigned to trigger inout 6.
- TRIG_IN_7 = 14 # Trigger output assigned to trigger inout 7.
- TRIG_IN_8 = 15 # Trigger output assigned to trigger inout 8.
- HIGH = 17 # Trigger output is set to high.
- LOW = 18 # Trigger output is set to low.
-
-
-class HDAWGChannelGrouping(Enum):
- """How many independent sequencers should run on the AWG and how the outputs should be grouped by sequencer."""
- MDS = -1 # All channels that are in the current multi device synchronized group
- CHAN_GROUP_4x2 = 0 # 4x2 with HDAWG8; 2x2 with HDAWG4. /dev.../awgs/0..3/
- CHAN_GROUP_2x4 = 1 # 2x4 with HDAWG8; 1x4 with HDAWG4. /dev.../awgs/0 & 2/
- CHAN_GROUP_1x8 = 2 # 1x8 with HDAWG8. /dev.../awgs/0/
-
- def group_size(self) -> int:
- return {
- HDAWGChannelGrouping.CHAN_GROUP_4x2: 2,
- HDAWGChannelGrouping.CHAN_GROUP_2x4: 4,
- HDAWGChannelGrouping.CHAN_GROUP_1x8: 8,
- HDAWGChannelGrouping.MDS: None
- }[self]
-
-
-class HDAWGVoltageRange(Enum):
- """All available voltage ranges for the HDAWG wave outputs. Define maximum output voltage."""
- RNG_5V = 5
- RNG_4V = 4
- RNG_3V = 3
- RNG_2V = 2
- RNG_1V = 1
- RNG_800mV = 0.8
- RNG_600mV = 0.6
- RNG_400mV = 0.4
- RNG_200mV = 0.2
-
-
-class HDAWGModulationMode(Enum):
- """Modulation mode of waveform generator."""
- OFF = 0 # AWG output goes directly to signal output.
- SINE_1 = 1 # AWG output multiplied with sine generator signal 0.
- SINE_2 = 2 # AWG output multiplied with sine generator signal 1.
- FG_1 = 3 # AWG output multiplied with function generator signal 0. Requires FG option.
- FG_2 = 4 # AWG output multiplied with function generator signal 1. Requires FG option.
- ADVANCED = 5 # AWG output modulates corresponding sines from modulation carriers.
-
-
-@traced
-class HDAWGChannelGroup(AWG):
- MIN_WAVEFORM_LEN = 192
- WAVEFORM_LEN_QUANTUM = 16
-
- def __init__(self,
- identifier: str,
- timeout: float) -> None:
- super().__init__(identifier)
- self.timeout = timeout
-
- self._awg_module = None
- self._program_manager = HDAWGProgramManager()
- self._elf_manager = None
- self._required_seqc_source = self._program_manager.to_seqc_program()
- self._uploaded_seqc_source = None
- self._current_program = None # Currently armed program.
- self._upload_generator = ()
-
- self._master_device = None
-
- def _initialize_awg_module(self):
- """Only run once"""
- if self._awg_module:
- self._awg_module.clear()
- self._awg_module = self.master_device.api_session.awgModule()
- self._awg_module.set('awgModule/device', self.master_device.serial)
- self._awg_module.set('awgModule/index', self.awg_group_index)
- self._awg_module.execute()
- self._elf_manager = ELFManager(self._awg_module)
- self._upload_generator = ()
-
- @property
- def master_device(self) -> HDAWGRepresentation:
- """Reference to HDAWG representation."""
- if self._master_device is None:
- raise HDAWGValueError('Channel group is currently not connected')
- return self._master_device
-
- @property
- def awg_module(self) -> zhinst_core.AwgModule:
- """Each AWG channel group has its own awg module to manage program compilation and upload."""
- if self._awg_module is None:
- raise HDAWGValueError('Channel group is not connected and was never initialized')
- return self._awg_module
-
- @property
- @abstractmethod
- def awg_group_index(self) -> int:
- raise NotImplementedError()
-
- @property
- def num_markers(self) -> int:
- """Number of marker channels"""
- return 2 * self.num_channels
-
- def upload(self, name: str,
- program: Loop,
- channels: Tuple[Optional[ChannelID], ...],
- markers: Tuple[Optional[ChannelID], ...],
- voltage_transformation: Tuple[Callable, ...],
- force: bool = False) -> None:
- """Upload a program to the AWG.
-
- Physically uploads all waveforms required by the program - excluding those already present -
- to the device and sets up playback sequences accordingly.
- This method should be cheap for program already on the device and can therefore be used
- for syncing. Programs that are uploaded should be fast(~1 sec) to arm.
-
- Args:
- name: A name for the program on the AWG.
- program: The program (a sequence of instructions) to upload.
- channels: Tuple of length num_channels that ChannelIDs of in the program to use. Position in the list
- corresponds to the AWG channel
- markers: List of channels in the program to use. Position in the List in the list corresponds to
- the AWG channel
- voltage_transformation: transformations applied to the waveforms extracted rom the program. Position
- in the list corresponds to the AWG channel
- force: If a different sequence is already present with the same name, it is
- overwritten if force is set to True. (default = False)
-
- Known programs are handled in host memory most of the time. Only when uploading the
- device memory is touched at all.
-
- Returning from setting user register in seqc can take from 50ms to 60 ms. Fluctuates heavily. Not a good way to
- have deterministic behaviour "setUserReg(PROG_SEL, PROG_IDLE);".
- """
- if len(channels) != self.num_channels:
- raise HDAWGValueError('Channel ID not specified')
- if len(markers) != self.num_markers:
- raise HDAWGValueError('Markers not specified')
- if len(voltage_transformation) != self.num_channels:
- raise HDAWGValueError('Wrong number of voltage transformations')
-
- if name in self.programs and not force:
- raise HDAWGValueError('{} is already known on {}'.format(name, self.identifier))
-
- # Go to qupulse nanoseconds time base.
- q_sample_rate = self.sample_rate / 10**9
-
- # Adjust program to fit criteria.
- make_compatible(program,
- minimal_waveform_length=self.MIN_WAVEFORM_LEN,
- waveform_quantum=self.WAVEFORM_LEN_QUANTUM,
- sample_rate=q_sample_rate)
-
- if self._amplitude_offset_handling == AWGAmplitudeOffsetHandling.IGNORE_OFFSET:
- voltage_offsets = (0.,) * self.num_channels
- elif self._amplitude_offset_handling == AWGAmplitudeOffsetHandling.CONSIDER_OFFSET:
- voltage_offsets = self.offsets()
- else:
- raise ValueError('{} is invalid as AWGAmplitudeOffsetHandling'.format(self._amplitude_offset_handling))
-
- amplitudes = self.amplitudes()
-
- if name in self._program_manager.programs:
- self._program_manager.remove(name)
-
- self._program_manager.add_program(name,
- program,
- channels=channels,
- markers=markers,
- voltage_transformations=voltage_transformation,
- sample_rate=q_sample_rate,
- amplitudes=amplitudes,
- offsets=voltage_offsets)
-
- self._required_seqc_source = self._program_manager.to_seqc_program()
- self._program_manager.waveform_memory.sync_to_file_system(self.master_device.waveform_file_system)
-
- # start compiling the source (non-blocking)
- self._start_compile_and_upload()
-
- def _start_compile_and_upload(self):
- self._uploaded_seqc_source = None
- self._upload_generator = self._elf_manager.compile_and_upload(self._required_seqc_source)
-
- def _wait_for_compile_and_upload(self):
- for state in self._upload_generator:
- logger.debug("wait_for_compile_and_upload: %r", state)
- time.sleep(.1)
- self._uploaded_seqc_source = self._required_seqc_source
- logger.debug("AWG %d: wait_for_compile_and_upload has finished", self.awg_group_index)
-
- def was_current_program_finished(self) -> bool:
- """Return true if the current program has finished at least once"""
- playback_finished_mask = int(HDAWGProgramManager.Constants.PLAYBACK_FINISHED_MASK, 2)
- return bool(self.user_register(HDAWGProgramManager.Constants.PROG_SEL_REGISTER) & playback_finished_mask)
-
- def set_volatile_parameters(self, program_name: str, parameters: Mapping[str, numbers.Real]):
- """Set the values of parameters which were marked as volatile on program creation."""
- new_register_values = self._program_manager.get_register_values_to_update_volatile_parameters(program_name,
- parameters)
- if self._current_program == program_name:
- for register, value in new_register_values.items():
- self.user_register(register, value)
-
- def remove(self, name: str) -> None:
- """Remove a program from the AWG.
-
- Also discards all waveforms referenced only by the program identified by name.
-
- Args:
- name: The name of the program to remove.
- """
- self._program_manager.remove(name)
- self._required_seqc_source = self._program_manager.to_seqc_program()
-
- def clear(self) -> None:
- """Removes all programs and waveforms from the AWG.
-
- Caution: This affects all programs and waveforms on the AWG, not only those uploaded using qupulse!
- """
- self._program_manager.clear()
- self._current_program = None
- self._required_seqc_source = self._program_manager.to_seqc_program()
- self._start_compile_and_upload()
- self.arm(None)
-
- def arm(self, name: Optional[str]) -> None:
- """Load the program 'name' and arm the device for running it. If name is None the awg will "dearm" its current
- program.
-
- Currently hardware triggering is not implemented. The HDAWGProgramManager needs to emit code that calls
- `waitDigTrigger` to do that.
- """
- if self.num_channels > 8:
- if name is None:
- self._required_seqc_source = ""
- else:
- self._required_seqc_source = self._program_manager.to_seqc_program(name)
- self._start_compile_and_upload()
-
- if self._required_seqc_source != self._uploaded_seqc_source:
- self._wait_for_compile_and_upload()
-
- self.user_register(self._program_manager.Constants.TRIGGER_REGISTER, 0)
-
- if name is None:
- self.user_register(self._program_manager.Constants.PROG_SEL_REGISTER,
- self._program_manager.Constants.PROG_SEL_NONE)
- self._current_program = None
- else:
- if name not in self.programs:
- raise HDAWGValueError('{} is unknown on {}'.format(name, self.identifier))
- self._current_program = name
-
- # set the registers of initial repetition counts
- for register, value in self._program_manager.get_register_values(name).items():
- assert register not in (self._program_manager.Constants.PROG_SEL_REGISTER,
- self._program_manager.Constants.TRIGGER_REGISTER)
- self.user_register(register, value)
-
- self.user_register(self._program_manager.Constants.PROG_SEL_REGISTER,
- self._program_manager.name_to_index(name) | int(self._program_manager.Constants.NO_RESET_MASK, 2))
-
- # this was a workaround for problems in the past and I totally forgot why it was here
- # for ch_pair in self.master.channel_tuples:
- # ch_pair._wait_for_compile_and_upload()
- self.enable(True)
-
- def run_current_program(self) -> None:
- """Run armed program."""
- if self._current_program is not None:
- if self._current_program not in self.programs:
- raise HDAWGValueError('{} is unknown on {}'.format(self._current_program, self.identifier))
- if not self.enable():
- self.enable(True)
- self.user_register(self._program_manager.Constants.TRIGGER_REGISTER,
- int(self._program_manager.Constants.TRIGGER_RESET_MASK, 2))
- else:
- raise HDAWGRuntimeError('No program active')
-
- @property
- def programs(self) -> Set[str]:
- """The set of program names that can currently be executed on the hardware AWG."""
- return set(self._program_manager.programs.keys())
-
- @property
- def sample_rate(self) -> TimeType:
- """The default sample rate of the AWG channel group."""
- node_path = '/{}/awgs/{}/time'.format(self.master_device.serial, self.awg_group_index)
- sample_rate_num = self.master_device.api_session.getInt(node_path)
- node_path = '/{}/system/clocks/sampleclock/freq'.format(self.master_device.serial)
- sample_clock = self.master_device.api_session.getDouble(node_path)
-
- """Calculate exact rational number based on (sample_clock Sa/s) / 2^sample_rate_num. Otherwise numerical
- imprecision will give rise to errors for very long pulses. fractions.Fraction does not accept floating point
- numerator, which sample_clock could potentially be."""
- return time_from_float(sample_clock) / 2 ** sample_rate_num
-
- def connect_group(self, hdawg_device: HDAWGRepresentation):
- self.disconnect_group()
- self._master_device = weakref.proxy(hdawg_device)
- self._initialize_awg_module()
- # Seems creating AWG module sets SINGLE (single execution mode of sequence) to 0 per default.
- self.master_device.api_session.setInt(f'/{self.master_device.serial}/awgs/0/single', 1)
-
- def disconnect_group(self):
- """Disconnect this group from device so groups of another size can be used"""
- if self._awg_module:
- self.awg_module.clear()
- self._master_device = None
- self._elf_manager = None
- self._upload_generator = ()
-
- def is_connected(self) -> bool:
- return self._master_device is not None
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
- def user_register(self, reg: UserRegister, value: int = None) -> int:
- """Query user registers (1-16) and optionally set it.
-
- Args:
- reg: User register. If it is an int, a warning is raised and it is interpreted as a one based index
- value: Value to set
-
- Returns:
- User Register value after setting it
- """
- if isinstance(reg, int):
- warnings.warn("User register is not a UserRegister instance. It is interpreted as one based index.")
- reg = UserRegister(one_based_value=reg)
-
- if reg.to_web_interface() not in range(1, 17):
- raise HDAWGValueError(f'{reg:!r} not a valid (1-16) register.')
-
- node_path = '/{}/awgs/{:d}/userregs/{:labone}'.format(self.master_device.serial, self.awg_group_index, reg)
- if value is not None:
- self.master_device.api_session.setInt(node_path, value)
- # hackedy
- for mds_serial in getattr(self, '_mds_devices', [])[1:]:
- self.master_device.api_session.setInt(node_path.replace(self.master_device.serial, mds_serial), value)
- self.master_device.api_session.sync() # Global sync: Ensure settings have taken effect on the device.
- return self.master_device.api_session.getInt(node_path)
-
-
-@traced
-class MDSChannelGroup(HDAWGChannelGroup):
- def __init__(self,
- identifier: str,
- timeout: float) -> None:
- super().__init__(identifier, timeout)
-
- self._master_device = None
- self._mds_devices = None
-
- @property
- def num_channels(self) -> int:
- """Number of channels"""
- return len(self._mds_devices) * 8
-
- @property
- def awg_group_index(self):
- return 0
-
- def disconnect_group(self):
- super().disconnect_group()
- self._mds_devices = None
-
- def connect_group(self, hdawg_device: HDAWGRepresentation):
- mds_group = hdawg_device._get_mds_group_idx()
- if mds_group is None:
- raise HDAWGException("AWG not in any MDS group", hdawg_device)
- mds_devices = hdawg_device.api_session.getString(f'/ZI/MDS/GROUPS/{mds_group}/DEVICES').split(',')
- if hdawg_device.serial != mds_devices[0]:
- raise HDAWGException("Only the master device can connect to the HDAWG MDS channel group.")
- super().connect_group(hdawg_device)
- self._mds_devices = mds_devices
-
- def enable(self, status: bool = None) -> bool:
- """Start the AWG sequencer."""
- # There is also 'awgModule/awg/enable', which seems to have the same functionality.
- node_path = '/{}/awgs/{:d}/enable'.format(self.master_device.serial, 0)
- if status is not None:
- self.awg_module.set('awg/enable', int(status))
- else:
- status = self.awg_module.get('awg/module')
-
- #return bool(status)
- """
- if status is not None:
- self.master_device.api_session.setInt(node_path, int(status))
- for mds_device in self._mds_devices[1:]:
- self.master_device.api_session.setInt(node_path.replace(self._mds_devices[0], mds_device), int(status))
- self.master_device.api_session.sync() # Global sync: Ensure settings have taken effect on the device.
- """
- return bool(self.master_device.api_session.getInt(node_path))
-
- def amplitudes(self) -> Tuple[float, ...]:
- """Query AWG channel amplitude value (not peak to peak).
-
- From manual:
- The final signal amplitude is given by the product of the full scale
- output range of 1 V[in this example], the dimensionless amplitude
- scaling factor 1.0, and the actual dimensionless signal amplitude
- stored in the waveform memory."""
- amplitudes = []
-
- api_session = self.master_device.api_session
- for mds_device in self._mds_devices:
- amplitude_scales = _amplitude_scales(api_session, mds_device)
- ranges = [_sigout_range(api_session, mds_device, ch) for ch in range(1, 9)]
- amplitudes.extend(zi_amplitude * zi_range / 2 for zi_amplitude, zi_range in zip(amplitude_scales, ranges))
- return tuple(amplitudes)
-
- def offsets(self) -> Tuple[float, ...]:
- offsets = []
- api_session = self.master_device.api_session
- for mds_device in self._mds_devices:
- offsets.extend(_sigout_offset(api_session, mds_device, ch) for ch in range(1, 9))
- return tuple(offsets)
-
-
-class SingleDeviceChannelGroup(HDAWGChannelGroup):
- def __init__(self,
- group_idx: int,
- group_size: int,
- identifier: str,
- timeout: float) -> None:
- super().__init__(identifier, timeout)
- self._device = None
-
- assert group_idx in range(4)
- assert group_size in (2, 4, 8)
-
- self._group_idx = group_idx
- self._group_size = group_size
-
- @property
- def num_channels(self) -> int:
- """Number of channels"""
- return self._group_size
-
- def _channels(self, index_start=1) -> Tuple[int, ...]:
- """1 indexed channel"""
- offset = index_start + self._group_size * self._group_idx
- return tuple(ch + offset for ch in range(self.num_channels))
-
- @property
- def awg_group_index(self) -> int:
- """AWG node group index assuming 4x2 channel grouping. Then 0...3 will give appropriate index of group."""
- return self._group_idx
-
- @property
- def user_directory(self) -> str:
- """LabOne user directory with subdirectories: "awg/src" (seqc sourcefiles), "awg/elf" (compiled AWG binaries),
- "awag/waves" (user defined csv waveforms)."""
- return self.awg_module.getString('awgModule/directory')
-
- def enable(self, status: bool = None) -> bool:
- """Start the AWG sequencer."""
- # There is also 'awgModule/awg/enable', which seems to have the same functionality.
- node_path = '/{}/awgs/{:d}/enable'.format(self.master_device.serial, self.awg_group_index)
- if status is not None:
- self.master_device.api_session.setInt(node_path, int(status))
- self.master_device.api_session.sync() # Global sync: Ensure settings have taken effect on the device.
- return bool(self.master_device.api_session.getInt(node_path))
-
- def amplitudes(self) -> Tuple[float, ...]:
- """Query AWG channel amplitude value (not peak to peak).
-
- From manual:
- The final signal amplitude is given by the product of the full scale
- output range of 1 V[in this example], the dimensionless amplitude
- scaling factor 1.0, and the actual dimensionless signal amplitude
- stored in the waveform memory."""
- amplitudes = []
-
- for ch, zi_amplitude in zip(self._channels(), _amplitude_scales(self.master_device.api_session, self.master_device.serial)):
- zi_range = self.master_device.range(ch)
- amplitudes.append(zi_amplitude * zi_range / 2)
- return tuple(amplitudes)
-
- def offsets(self) -> Tuple[float, ...]:
- return tuple(map(self.master_device.offset, self._channels()))
-
-
-class ELFManager:
- class AWGModule:
- def __init__(self, awg_module: zhinst_core.AwgModule):
- """Provide an easily mockable interface to the zhinst AwgModule object"""
- self._module = awg_module
-
- @property
- def src_dir(self) -> pathlib.Path:
- return pathlib.Path(self._module.getString('directory'), 'awg', 'src')
-
- @property
- def elf_dir(self) -> pathlib.Path:
- return pathlib.Path(self._module.getString('directory'), 'awg', 'elf')
-
- @property
- def compiler_start(self) -> bool:
- """True if the compiler is running"""
- return self._module.getInt('compiler/start') == 1
-
- @compiler_start.setter
- def compiler_start(self, value: bool):
- """Set true to start the compiler"""
- self._module.set('compiler/start', value)
-
- @property
- def compiler_status(self) -> Tuple[int, str]:
- return self._module.getInt('compiler/status'), self._module.getString('compiler/statusstring')
-
- @property
- def compiler_source_file(self) -> str:
- return self._module.getString('compiler/sourcefile')
-
- @compiler_source_file.setter
- def compiler_source_file(self, source_file: str):
- self._module.set('compiler/sourcefile', source_file)
-
- @property
- def compiler_upload(self) -> bool:
- """auto upload after compiling"""
- return self._module.getInt('compiler/upload') == 1
-
- @compiler_upload.setter
- def compiler_upload(self, value: bool):
- self._module.set('compiler/upload', value)
-
- @property
- def elf_file(self) -> str:
- return self._module.getString('elf/file')
-
- @elf_file.setter
- def elf_file(self, elf_file: str):
- self._module.set('elf/file', elf_file)
-
- @property
- def elf_upload(self) -> bool:
- return bool(self._module.getInt('elf/upload'))
-
- @elf_upload.setter
- def elf_upload(self, value: bool):
- self._module.set('elf/upload', value)
-
- @property
- def elf_status(self) -> Tuple[int, float]:
- return self._module.getInt('elf/status'), self._module.getDouble('progress')
-
- @property
- def index(self) -> int:
- return self._module.getInt('index')
-
- def __init__(self, awg_module: zhinst_core.AwgModule):
- """This class organizes compiling and uploading of compiled programs. The source code file is named based on the
- code hash to cache compilation results. This requires that the waveform names are unique.
-
- The compilation and upload itself are done asynchronously by zhinst.core. To avoid spawning a useless
- thread for updating the status the method :py:meth:`~ELFManager.compile_and_upload` returns a generator which
- talks to the undelying library when needed."""
- self.awg_module = self.AWGModule(awg_module)
-
- # automatically upload after successful compilation
- self.awg_module.compiler_upload = True
-
- self._compile_job = None # type: Optional[Union[str, Tuple[str, int, str]]]
- self._upload_job = None # type: Optional[Union[Tuple[str, float], Tuple[str, int]]]
-
- def clear(self):
- """Deletes all files with a SHA512 hash name"""
- src_regex = re.compile(r'[a-z0-9]{128}\.seqc')
- elf_regex = re.compile(r'[a-z0-9]{128}\.elf')
-
- for p in self.awg_module.src_dir.iterdir():
- if src_regex.match(p.name):
- p.unlink()
-
- for p in self.awg_module.elf_dir.iterdir():
- if elf_regex.match(p.name):
- p.unlink()
-
- @staticmethod
- def _source_hash(source_string: str) -> str:
- """Calulate the SHA512 hash of the given source.
-
- Args:
- source_string: seqc source code
-
- Returns:
- hex representation of SHA512 `source_string` hash
- """
- # use utf-16 because str is UTF16 on most relevant machines (Windows)
- return hashlib.sha512(bytes(source_string, 'utf-16')).hexdigest()
-
- def _update_compile_job_status(self):
- """Store current compile status in self._compile_job."""
- compiler_start = self.awg_module.compiler_start
- if self._compile_job is None:
- assert compiler_start == 0
-
- elif isinstance(self._compile_job, str):
- if compiler_start:
- # compilation is running
- pass
-
- else:
- compiler_status, status_string = self.awg_module.compiler_status
- assert compiler_status in (-1, 0, 1, 2)
- if compiler_status == -1:
- raise RuntimeError('Compile job is set but no compilation is running', status_string)
- elif compiler_status == 2:
- logger.warning("AWG %d: Compilation finished with warning: %s", self.awg_module.index, status_string)
- self._compile_job = (self._compile_job, compiler_status, status_string)
-
- def _start_compile_job(self, source_file):
- logger.debug("Starting compilation of %r", source_file)
- self._update_compile_job_status()
- assert not isinstance(self._compile_job, str)
- self.awg_module.compiler_source_file = source_file
- self.awg_module.compiler_start = True
- self._compile_job = source_file
- logger.debug("AWG %d: Compilation of %r started", self.awg_module.index, source_file)
-
- def _compile(self, source_file) -> Generator[str, str, None]:
- self._start_compile_job(source_file)
-
- while True:
- self._update_compile_job_status()
- if not isinstance(self._compile_job, str):
- # finished compiling
- logger.debug("AWG %d: Compilation of %r finished", self.awg_module.index, source_file)
- break
- cmd = yield 'compiling'
- if cmd is None:
- logger.debug('No command received during compiling')
- elif cmd == 'abort':
- raise NotImplementedError('clean abort not implemented')
- else:
- raise HDAWGValueError('Unknown command', cmd)
-
- _, status_int, status_str = self._compile_job
- if status_int == 1:
- raise HDAWGRuntimeError('Compilation failed', status_str)
- logger.info("AWG %d: Compilation of %r successful", self.awg_module.index, source_file)
-
- def _start_elf_upload(self, elf_file):
- logger.debug("Uploading %r", elf_file)
- current_elf = self.awg_module.elf_file
- if current_elf != elf_file:
- logger.info("AWG %d: Overwriting elf file", self.awg_module.index)
- self.awg_module.elf_file = elf_file
- self.awg_module.elf_upload = True
- self._upload_job = (elf_file, None)
- time.sleep(.001)
-
- def _update_upload_job_status(self):
- elf_upload = self.awg_module.elf_upload
- if self._upload_job is None:
- assert not elf_upload
- return
-
- elf_file, old_status = self._upload_job
- assert self.awg_module.elf_file == elf_file
-
- if isinstance(old_status, float) or old_status is None:
- status_int, progress = self.awg_module.elf_status
- if status_int == 2:
- # in progress
- assert elf_upload == 1
- self._upload_job = elf_file, progress
- else:
- # fetch new value here
- self._upload_job = elf_file, status_int
-
- else:
- logger.debug('AWG %d: _update_upload_job_status called on finished upload', self.awg_module.index)
- assert elf_upload == 0
-
- def _upload(self, elf_file) -> Generator[str, str, None]:
- if self.awg_module.compiler_upload:
- pass
- else:
- self._start_elf_upload(elf_file)
-
- while True:
- self._update_upload_job_status()
- _, status = self._upload_job
- if isinstance(status, int):
- assert status in (-1, 0, 1)
- if status == 1:
- raise RuntimeError('ELF upload failed')
- else:
- break
- else:
- progress = status
- logger.debug('AWG %d: Upload progress is %d%%', self.awg_module.index, progress*100)
-
- cmd = yield 'uploading @ %d%%' % (100*progress)
- if cmd is None:
- logger.debug("No command received during upload")
- if cmd == 'abort':
- # TODO: check if this stops the upload
- self.awg_module.elf_upload = False
- raise NotImplementedError('Abort upload not cleanly implemented')
- else:
- raise HDAWGValueError('Unknown command', cmd)
-
- # enable auto upload on compilation again
- # TODO: research whether this is necessary
- # self.awg_module.elf_file = ''
-
- def compile_and_upload(self, source_string: str) -> Generator[str, str, None]:
- """The source code is saved to a file determined by the source hash, compiled and uploaded to the instrument.
- The function returns a generator that yields the current state of the progress. The generator is empty iff the
- upload is complete. An exception is raised if there is an error.
-
- To abort send 'abort' to the generator.
-
- Example:
- >>> my_source = 'playWave("my_wave");'
- >>> for state in elf_manager.compile_and_upload(my_source):
- ... print('Current state:', state)
- ... time.sleep(1)
-
- Args:
- source_string: Source code to compile
-
- Returns:
- Generator object that needs to be consumed
- """
- self._update_compile_job_status()
- if isinstance(self._compile_job, str):
- raise NotImplementedError('cannot upload: compilation in progress')
-
- source_hash = self._source_hash(source_string)
-
- seqc_file_name = '%s.seqc' % source_hash
- elf_file_name = '%s.elf' % source_hash
-
- full_source_name = self.awg_module.src_dir.joinpath(seqc_file_name)
- full_elf_name = self.awg_module.elf_dir.joinpath(elf_file_name)
-
- if not full_source_name.exists():
- full_source_name.write_text(source_string, 'utf-8')
-
- # we assume same source == same program here
- if not full_elf_name.exists():
- yield from self._compile(seqc_file_name)
- else:
- # set this so the web interface shows the correct source
- # self.awg_module.compiler_source_file = seqc_file_name
- logger.info('Already compiled. ELF: %r', elf_file_name)
-
- yield from self._upload(elf_file_name)
-
-
-class HDAWGException(Exception):
- """Base exception class for HDAWG errors."""
- pass
-
-
-class HDAWGValueError(HDAWGException, ValueError):
- pass
-
-
-class HDAWGTypeError(HDAWGException, TypeError):
- pass
-
-
-class HDAWGRuntimeError(HDAWGException, RuntimeError):
- pass
-
-
-class HDAWGIOError(HDAWGException, IOError):
- pass
-
-
-class HDAWGTimeoutError(HDAWGException, TimeoutError):
- pass
-
-
-class HDAWGCompilationException(HDAWGException):
- def __init__(self, msg):
- self.msg = msg
-
- def __str__(self) -> str:
- return "Compilation failed: {}".format(self.msg)
-
-
-class HDAWGUploadException(HDAWGException):
- def __str__(self) -> str:
- return "Upload to the instrument failed."
-
-
-def get_group_for_channels(hdawg: HDAWGRepresentation, channels: Set[int]) -> HDAWGChannelGroup:
- channels = set(channels)
- assert not channels - set(range(8)), "Channels must be in 0..=7"
+import sys
+import argparse
+import logging
- channel_range = range(min(channels) // 2 * 2, (max(channels) + 2) // 2 * 2)
- if len(channel_range) > 4 or len(channel_range) == 4 and channel_range.start == 2:
- c = (HDAWGChannelGrouping.CHAN_GROUP_1x8, 0)
- elif len(channel_range) == 4:
- assert channel_range.start in (0, 4)
- c = (HDAWGChannelGrouping.CHAN_GROUP_2x4, channel_range.start // 4)
- else:
- assert len(channel_range) == 2
- c = (HDAWGChannelGrouping.CHAN_GROUP_4x2, channel_range.start // 2)
+from typing import Set
- hdawg.channel_grouping = c[0]
- return hdawg.channel_tuples[c[1]]
+if sys.version_info.minor > 8:
+ try:
+ from qupulse_hdawg.zihdawg import *
+ except ImportError:
+ print("Install the qupulse_hdawg package to use HDAWG with this python version.")
+ raise
+else:
+ try:
+ from qupulse_hdawg_legacy.zihdawg import *
+ except ImportError:
+ print("Install the qupulse_hdawg_legacy package to use HDAWG with this python version.")
+ raise
def example_upload(hdawg_kwargs: dict, channels: Set[int], markers: Set[Tuple[int, int]]): # pragma: no cover
@@ -1227,5 +119,4 @@ def example_upload(hdawg_kwargs: dict, channels: Set[int], markers: Set[Tuple[in
markers = [(m // 2, m % 2) for m in parsed.pop('markers')]
logging.basicConfig(stream=sys.stdout)
- logger.setLevel(logging.DEBUG)
example_upload(hdawg_kwargs=parsed, channels=channels, markers=markers)
diff --git a/qupulse/hardware/dacs/__init__.py b/qupulse/hardware/dacs/__init__.py
index 99a13581c..7d001f3e3 100644
--- a/qupulse/hardware/dacs/__init__.py
+++ b/qupulse/hardware/dacs/__init__.py
@@ -1,6 +1,14 @@
-from qupulse.hardware.dacs.dac_base import *
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
-try:
- from qupulse.hardware.dacs.alazar import *
-except ImportError:
- pass
+import lazy_loader as lazy
+
+__getattr__, __dir__, __all__ = lazy.attach(
+ __name__,
+ submodules={'alazar2'},
+ submod_attrs={
+ 'dac_base': ['DAC'],
+ 'alazar': ['AlazarCard'],
+ }
+)
diff --git a/qupulse/hardware/dacs/alazar.py b/qupulse/hardware/dacs/alazar.py
index 7398e2115..f5a5ad308 100644
--- a/qupulse/hardware/dacs/alazar.py
+++ b/qupulse/hardware/dacs/alazar.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
import dataclasses
from typing import Dict, Any, Optional, Tuple, List, Iterable, Callable, Sequence
from collections import defaultdict
@@ -16,7 +20,7 @@
from qupulse.utils.types import TimeType
from qupulse.hardware.dacs.dac_base import DAC
from qupulse.hardware.util import traced
-from qupulse.utils.performance import time_windows_to_samples
+from qupulse.utils.performance import time_windows_to_samples, shrink_overlapping_windows
logger = logging.getLogger(__name__)
@@ -283,8 +287,7 @@ def _make_mask(self, mask_id: str, begins, lengths) -> Mask:
if mask_type not in ('auto', 'cross_buffer', None):
warnings.warn("Currently only CrossBufferMask is implemented.")
- if np.any(begins[:-1]+lengths[:-1] > begins[1:]):
- raise ValueError('Found overlapping windows in begins')
+ begins, lengths = shrink_overlapping_windows(begins, lengths)
mask = CrossBufferMask()
mask.identifier = mask_id
@@ -391,13 +394,16 @@ def register_mask_for_channel(self, mask_id: str, hw_channel: int, mask_type='au
raise NotImplementedError('Currently only can do cross buffer mask')
self._mask_prototypes[mask_id] = (hw_channel, mask_type)
- def measure_program(self, channels: Iterable[str]) -> Dict[str, np.ndarray]:
+ def measure_program(self, channels: Iterable[str]|None = None) -> Dict[str, np.ndarray]:
"""
Get all measurements at once and write them in a dictionary.
"""
scanline_data = self.__card.extractNextScanline()
-
+
+ if channels is None: #just get all channels
+ channels = scanline_data.operationResults.keys()
+
scanline_definition = scanline_data.definition
operation_definitions = {operation.identifier: operation
for operation in scanline_definition.operations}
diff --git a/qupulse/hardware/dacs/alazar2.py b/qupulse/hardware/dacs/alazar2.py
index 641606d91..afe125555 100644
--- a/qupulse/hardware/dacs/alazar2.py
+++ b/qupulse/hardware/dacs/alazar2.py
@@ -1,3 +1,8 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+import dataclasses
from typing import Union, Iterable, Dict, Tuple, Mapping, Optional
from types import MappingProxyType
import logging
@@ -30,6 +35,13 @@ def __init__(self, atsaverage_card: 'atsaverage.core.AlazarCard'):
self._raw_data_mask = None
self.default_buffer_strategy: Optional[BufferStrategySettings] = None
+ # sadly this is required to associate masks with their corresponding channels
+ # the better place for this would be in the MeasurementMask class but we do not want to touch it
+ # to avoid breaking experiments
+ # TODO: possible improvement is wildcard/regex support but this is complicated to maintain
+ # (competing matches etc)
+ self._mask_name_to_hw_channel = {}
+
@property
def atsaverage_card(self):
return self._atsaverage_card
@@ -74,6 +86,13 @@ def _make_scanline_definition(self, program: AlazarProgram):
sample_rate_in_hz = int(sample_rate_in_ghz * 10 ** 9)
masks = program.masks(make_best_mask)
+ for mask in masks:
+ try:
+ mask.channel = self._mask_name_to_hw_channel[mask.identifier]
+ except KeyError as err:
+ raise KeyError(f"There was no hardware channel registered for the mask {mask!r}",
+ mask.identifier) from err
+
if sample_rate_in_ghz != program.sample_rate:
raise RuntimeError("Masks were registered with a different sample rate")
return create_scanline_definition(masks, program.operations,
@@ -130,3 +149,14 @@ def get_input_range(operation_id: str):
input_range = get_input_range(op_name)
data[op_name] = scanline_data.operationResults[op_name].getAsVoltage(input_range)
return data
+
+ def register_mask_for_channel(self, mask_id: str, hw_channel: int) -> None:
+ """
+
+ Args:
+ mask_id: Identifier of the measurement windows
+ hw_channel: Associated hardware channel (0, 1, 2, 3)
+ """
+ if hw_channel not in range(4):
+ raise ValueError('{} is not a valid hw channel'.format(hw_channel))
+ self._mask_name_to_hw_channel[mask_id] = hw_channel
diff --git a/qupulse/hardware/dacs/dac_base.py b/qupulse/hardware/dacs/dac_base.py
index e68802576..bf7a5616c 100644
--- a/qupulse/hardware/dacs/dac_base.py
+++ b/qupulse/hardware/dacs/dac_base.py
@@ -1,7 +1,14 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
from abc import ABCMeta, abstractmethod
-from typing import Dict, Tuple, Iterable
+from typing import Dict, Tuple, Iterable, TYPE_CHECKING
-import numpy
+if TYPE_CHECKING:
+ import numpy
+else:
+ numpy = None
__all__ = ['DAC']
@@ -10,8 +17,8 @@ class DAC(metaclass=ABCMeta):
"""Representation of a data acquisition card"""
@abstractmethod
- def register_measurement_windows(self, program_name: str, windows: Dict[str, Tuple[numpy.ndarray,
- numpy.ndarray]]) -> None:
+ def register_measurement_windows(self, program_name: str, windows: Dict[str, Tuple['numpy.ndarray',
+ 'numpy.ndarray']]) -> None:
"""Register measurement windows for a given program. Overwrites previously defined measurement windows for
this program.
@@ -24,8 +31,8 @@ def register_measurement_windows(self, program_name: str, windows: Dict[str, Tup
@abstractmethod
def set_measurement_mask(self, program_name: str, mask_name: str,
- begins: numpy.ndarray,
- lengths: numpy.ndarray) -> Tuple[numpy.ndarray, numpy.ndarray]:
+ begins: 'numpy.ndarray',
+ lengths: 'numpy.ndarray') -> Tuple['numpy.ndarray', 'numpy.ndarray']:
"""Set/overwrite a single the measurement mask for a program. Begins and lengths are in nanoseconds.
Args:
@@ -60,5 +67,5 @@ def clear(self) -> None:
"""Clears all registered programs."""
@abstractmethod
- def measure_program(self, channels: Iterable[str]) -> Dict[str, numpy.ndarray]:
+ def measure_program(self, channels: Iterable[str]) -> Dict[str, 'numpy.ndarray']:
"""Get the last measurement's results of the specified operations/channels"""
diff --git a/qupulse/hardware/dacs/dummy.py b/qupulse/hardware/dacs/dummy.py
new file mode 100644
index 000000000..d80251d64
--- /dev/null
+++ b/qupulse/hardware/dacs/dummy.py
@@ -0,0 +1,50 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+from typing import Tuple, Set, Dict
+from collections import deque
+
+from qupulse.hardware.dacs.dac_base import DAC
+
+class DummyDAC(DAC):
+ """Dummy DAC for automated testing, debugging and usage in examples. """
+
+ def __init__(self):
+ self._measurement_windows = dict()
+ self._operations = dict()
+ self.measured_data = deque([])
+ self._meas_masks = {}
+ self._armed_program = None
+
+ @property
+ def armed_program(self):
+ return self._armed_program
+
+ def register_measurement_windows(self, program_name: str, windows: Dict[str, Tuple['numpy.ndarray',
+ 'numpy.ndarray']]):
+ self._measurement_windows[program_name] = windows
+
+ def register_operations(self, program_name: str, operations):
+ self._operations[program_name] = operations
+
+ def arm_program(self, program_name: str):
+ self._armed_program = program_name
+
+ def delete_program(self, program_name):
+ if program_name in self._operations:
+ self._operations.pop(program_name)
+ if program_name in self._measurement_windows:
+ self._measurement_windows.pop(program_name)
+
+ def clear(self) -> None:
+ self._measurement_windows = dict()
+ self._operations = dict()
+ self._armed_program = None
+
+ def measure_program(self, channels):
+ return self.measured_data.pop()
+
+ def set_measurement_mask(self, program_name, mask_name, begins, lengths) -> Tuple['numpy.ndarray', 'numpy.ndarray']:
+ self._meas_masks.setdefault(program_name, {})[mask_name] = (begins, lengths)
+ return begins, lengths
diff --git a/qupulse/hardware/feature_awg/base.py b/qupulse/hardware/feature_awg/base.py
index 0ea254b96..74f04ceed 100644
--- a/qupulse/hardware/feature_awg/base.py
+++ b/qupulse/hardware/feature_awg/base.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
from abc import ABC, abstractmethod
from typing import Optional, Collection
import weakref
diff --git a/qupulse/hardware/feature_awg/base_features.py b/qupulse/hardware/feature_awg/base_features.py
index 7461bf046..7e1fd3bfb 100644
--- a/qupulse/hardware/feature_awg/base_features.py
+++ b/qupulse/hardware/feature_awg/base_features.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
from types import MappingProxyType
from typing import Callable, Generic, Mapping, Optional, Type, TypeVar
from abc import ABC
diff --git a/qupulse/hardware/feature_awg/channel_tuple_wrapper.py b/qupulse/hardware/feature_awg/channel_tuple_wrapper.py
index 10212153b..1aa96c760 100644
--- a/qupulse/hardware/feature_awg/channel_tuple_wrapper.py
+++ b/qupulse/hardware/feature_awg/channel_tuple_wrapper.py
@@ -1,7 +1,11 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
from typing import Tuple, Optional, Callable, Set
from qupulse import ChannelID
-from qupulse._program._loop import Loop
+from qupulse.program.loop import Loop
from qupulse.hardware.feature_awg.base import AWGChannelTuple
from qupulse.hardware.feature_awg.features import ProgramManagement, VolatileParameters
from qupulse.hardware.awgs.base import AWG
diff --git a/qupulse/hardware/feature_awg/features.py b/qupulse/hardware/feature_awg/features.py
index 1b82189cc..47f6c3be4 100644
--- a/qupulse/hardware/feature_awg/features.py
+++ b/qupulse/hardware/feature_awg/features.py
@@ -1,9 +1,13 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
from abc import ABC, abstractmethod
from typing import Callable, Optional, Set, Tuple, Dict, Union, Any, Mapping
from numbers import Real
from enum import Enum
-from qupulse._program._loop import Loop
+from qupulse.program.loop import Loop
from qupulse.hardware.feature_awg.base import AWGDeviceFeature, AWGChannelFeature, AWGChannelTupleFeature,\
AWGChannelTuple
from qupulse.utils.types import ChannelID
diff --git a/qupulse/hardware/feature_awg/tabor.py b/qupulse/hardware/feature_awg/tabor.py
index 4a08ab8b1..83e84f12e 100644
--- a/qupulse/hardware/feature_awg/tabor.py
+++ b/qupulse/hardware/feature_awg/tabor.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
import functools
import logging
import numbers
@@ -11,7 +15,7 @@
import numpy as np
from qupulse import ChannelID
-from qupulse._program._loop import Loop, make_compatible
+from qupulse.program.loop import Loop, make_compatible
from qupulse.hardware.feature_awg.channel_tuple_wrapper import ChannelTupleAdapter
from qupulse.hardware.feature_awg.features import ChannelSynchronization, AmplitudeOffsetHandling, VoltageRange, \
diff --git a/qupulse/hardware/setup.py b/qupulse/hardware/setup.py
index 646815742..9415c1d1e 100644
--- a/qupulse/hardware/setup.py
+++ b/qupulse/hardware/setup.py
@@ -1,18 +1,22 @@
-from typing import NamedTuple, Set, Callable, Dict, Tuple, Union, Iterable, Any, Mapping
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+from typing import NamedTuple, Set, Callable, Dict, Tuple, Union, Iterable, Any, Mapping, Sequence
from collections import defaultdict
import warnings
import numbers
from qupulse.hardware.awgs.base import AWG
from qupulse.hardware.dacs import DAC
-from qupulse._program._loop import Loop
+from qupulse.program.loop import Loop
from qupulse.utils.types import ChannelID
import numpy as np
-__all__ = ['PlaybackChannel', 'MarkerChannel', 'HardwareSetup']
+__all__ = ['PlaybackChannel', 'MarkerChannel', 'MeasurementMask', 'HardwareSetup']
class MeasurementMask:
@@ -92,17 +96,36 @@ def __init__(self):
def register_program(self, name: str,
program: Loop,
- run_callback=lambda: None, update=False) -> None:
+ run_callback=lambda: None,
+ update: bool = False,
+ measurements: Mapping[str, Tuple[np.ndarray, np.ndarray]] = None,
+ channels: Sequence[ChannelID] = None,
+ ) -> None:
+ """Register a program under a given name at the hardware setup. The program will be uploaded to the
+ participating AWGs and DACs. The run callback is used for triggering the program after arming.
+
+ Args:
+ name: Name of the program.
+ program: Output of :py:meth:`~PulseTemplate.create_program`
+ run_callback: Used to trigger the program after arming
+ update: Must be set if the program is already known.
+ measurements: Will be used as measurements if provided. Otherwise, the measurements are extracted from the program.
+
+ """
if not callable(run_callback):
raise TypeError('The provided run_callback is not callable')
-
- channels = next(program.get_depth_first_iterator()).waveform.defined_channels
+
+ if channels is None:
+ channels = program.get_defined_channels()
if channels - set(self._channel_map.keys()):
raise KeyError('The following channels are unknown to the HardwareSetup: {}'.format(
channels - set(self._channel_map.keys())))
+ if measurements is None:
+ measurements = program.get_measurement_windows(drop=True)
+
temp_measurement_windows = defaultdict(list)
- for mw_name, begins_lengths in program.get_measurement_windows(drop=True).items():
+ for mw_name, begins_lengths in measurements.items():
temp_measurement_windows[mw_name].append(begins_lengths)
if set(temp_measurement_windows.keys()) - set(self._measurement_map.keys()):
diff --git a/qupulse/hardware/util.py b/qupulse/hardware/util.py
index 30a9aae34..35dcd01d8 100644
--- a/qupulse/hardware/util.py
+++ b/qupulse/hardware/util.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
from typing import Collection, Sequence, Tuple, Union, Optional
import itertools
@@ -10,7 +14,7 @@ def traced(obj):
"""Noop traced that is used if autologging package is not available"""
return obj
-from qupulse._program.waveforms import Waveform
+from qupulse.program.waveforms import Waveform
from qupulse.utils.types import TimeType
from qupulse.utils import pairwise
@@ -22,7 +26,7 @@ def traced(obj):
njit = lambda x: x
try:
- import zhinst
+ import zhinst.utils
except ImportError: # pragma: no cover
zhinst = None
@@ -99,7 +103,7 @@ def voltage_to_uint16(voltage: np.ndarray, output_amplitude: float, output_offse
def find_positions(data: Sequence, to_find: Sequence) -> np.ndarray:
"""Find indices of the first occurrence of the elements of to_find in data. Elements that are not in data result in
-1"""
- data_sorter = np.argsort(data)
+ data_sorter = np.argsort(data, kind='stable')
pos_left = np.searchsorted(data, to_find, side='left', sorter=data_sorter)
pos_right = np.searchsorted(data, to_find, side='right', sorter=data_sorter)
@@ -246,7 +250,7 @@ def check_invalid_values(ch_data):
if ch2 is None:
ch2 = np.zeros(size)
else:
- check_invalid_values(ch1)
+ check_invalid_values(ch2)
marker_data = np.zeros(size, dtype=np.uint16)
for idx, marker in enumerate(markers):
if marker is not None:
diff --git a/qupulse/parameter_scope.py b/qupulse/parameter_scope.py
index a59f94a0b..5629b60e8 100644
--- a/qupulse/parameter_scope.py
+++ b/qupulse/parameter_scope.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
"""Contains various implementations of the parameter lookup interface :class:`.Scope`"""
from abc import abstractmethod
diff --git a/qupulse/plotting.py b/qupulse/plotting.py
new file mode 100644
index 000000000..8b20ead67
--- /dev/null
+++ b/qupulse/plotting.py
@@ -0,0 +1,329 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+"""This module defines plotting functionality for instantiated PulseTemplates using matplotlib.
+
+Classes:
+ - PlottingNotPossibleException.
+Functions:
+ - plot: Plot a pulse using matplotlib.
+"""
+
+from typing import Dict, Tuple, Any, Optional, Set, List, Union, Mapping
+from numbers import Real
+
+import matplotlib.pyplot as plt
+import numpy as np
+import warnings
+import operator
+import itertools
+import functools
+
+try:
+ from matplotlib import colormaps
+ get_cmap = colormaps.get_cmap
+except (ImportError, AttributeError): # pragma: no cover
+ # was deprecated in matplotlib 3.7, but we keep it around to allow this code to work with older versions
+ get_cmap = plt.get_cmap
+
+from qupulse.program import waveforms
+from qupulse.utils.types import ChannelID, MeasurementWindow, has_type_interface
+from qupulse.pulses.pulse_template import PulseTemplate
+from qupulse.program.waveforms import Waveform
+from qupulse.program.loop import Loop, to_waveform
+
+
+__all__ = ["render", "plot", "PlottingNotPossibleException"]
+
+
+def render(program: Union[Loop],
+ sample_rate: Real = 10.0,
+ render_measurements: bool = False,
+ time_slice: Tuple[Real, Real] = None,
+ plot_channels: Optional[Set[ChannelID]] = None) -> Tuple[np.ndarray, Dict[ChannelID, np.ndarray],
+ List[MeasurementWindow]]:
+ """'Renders' a pulse program.
+
+ Samples all contained waveforms into an array according to the control flow of the program.
+
+ Args:
+ program: The pulse (sub)program to render. Can be represented either by a Loop object or the more
+ old-fashioned InstructionBlock.
+ sample_rate: The sample rate in GHz.
+ render_measurements: If True, the third return value is a list of measurement windows.
+ time_slice: The time slice to be rendered. If None, the entire pulse will be shown.
+ plot_channels: Only channels in this set are rendered. If None, all will.
+
+ Returns:
+ A tuple (times, values, measurements). times is a numpy.ndarray of dimensions sample_count where
+ containing the time values. voltages is a dictionary of one numpy.ndarray of dimensions sample_count per
+ defined channel containing corresponding sampled voltage values for that channel.
+ measurements is a sequence of all measurements where each measurement is represented by a tuple
+ (name, start_time, duration).
+ """
+ if has_type_interface(program, Loop):
+ waveform, measurements = _render_loop(program, render_measurements=render_measurements)
+ else:
+ raise ValueError('Cannot render an object of type %r' % type(program), program)
+
+ if waveform is None:
+ return np.array([]), dict(), measurements
+
+ if plot_channels is None:
+ channels = waveform.defined_channels
+ else:
+ channels = waveform.defined_channels & plot_channels
+
+ if time_slice is None:
+ start_time, end_time = 0, waveform.duration
+
+ elif time_slice[1] < time_slice[0] or time_slice[0] < 0 or time_slice[1] < 0:
+ raise ValueError("time_slice is not valid.")
+
+ else:
+ start_time, end_time, *_ = time_slice
+
+ # filter measurement windows
+ measurements = [(name, begin, length)
+ for name, begin, length in measurements
+ if begin < end_time and begin + length > start_time]
+
+ sample_count = (end_time - start_time) * sample_rate + 1
+ if sample_count < 2:
+ raise PlottingNotPossibleException(pulse=None,
+ description='cannot render sequence with less than 2 data points')
+ if not round(float(sample_count), 10).is_integer():
+ warnings.warn(f"Sample count {sample_count} is not an integer. Will be rounded (this changes the sample rate).",
+ stacklevel=2)
+
+ times = np.linspace(float(start_time), float(end_time), num=int(sample_count))
+ times[-1] = np.nextafter(times[-1], times[-2])
+
+ voltages = {ch: waveforms._ALLOCATION_FUNCTION(times, **waveforms._ALLOCATION_FUNCTION_KWARGS)
+ for ch in channels}
+ for ch, ch_voltage in voltages.items():
+ waveform.get_sampled(channel=ch, sample_times=times, output_array=ch_voltage)
+
+ return times, voltages, measurements
+
+
+def _render_loop(loop: Loop,
+ render_measurements: bool,) -> Tuple[Waveform, List[MeasurementWindow]]:
+ """Transform program into single waveform and measurement windows.
+ The specific implementation of render for Loop arguments."""
+ waveform = to_waveform(loop)
+
+ if render_measurements:
+ measurement_dict = loop.get_measurement_windows()
+ measurement_list = []
+ for name, (begins, lengths) in measurement_dict.items():
+ measurement_list.extend(zip(itertools.repeat(name), begins, lengths))
+ measurements = sorted(measurement_list, key=operator.itemgetter(1))
+ else:
+ measurements = []
+
+ return waveform, measurements
+
+
+def plot(pulse: Union[PulseTemplate, Loop],
+ parameters: Optional[Dict[str, Real]] = None,
+ sample_rate: Optional[Real] = 10,
+ axes: Any=None,
+ show: bool=True,
+ plot_channels: Optional[Set[ChannelID]]=None,
+ plot_measurements: Optional[Set[str]]=None,
+ stepped: bool=True,
+ maximum_points: int=10**6,
+ time_slice: Tuple[Real, Real]=None,
+ **kwargs) -> Any: # pragma: no cover
+ """Plots a pulse using matplotlib.
+
+ The given pulse template will first be turned into a pulse program (represented by a Loop object) with the provided
+ parameters. The render() function is then invoked to obtain voltage samples over the entire duration of the pulse which
+ are then plotted in a matplotlib figure.
+
+ Args:
+ pulse: The pulse to be plotted.
+ parameters: An optional mapping of parameter names to Parameter
+ objects.
+ sample_rate: The rate with which the waveforms are sampled for the plot in
+ samples per time unit. If None, then automatically determine the sample rate (default = 10)
+ axes: matplotlib Axes object the pulse will be drawn into if provided
+ show: If true, the figure will be shown
+ plot_channels: If specified only channels from this set will be plotted. If omitted all channels will be.
+ stepped: If true pyplot.step is used for plotting
+ plot_measurements: If specified measurements in this set will be plotted. If omitted no measurements will be.
+ maximum_points: If the sampled waveform is bigger, it is not plotted
+ time_slice: The time slice to be plotted. If None, the entire pulse will be shown.
+ kwargs: Forwarded to pyplot. Overwrites other settings.
+ Returns:
+ matplotlib.pyplot.Figure instance in which the pulse is rendered
+ Raises:
+ PlottingNotPossibleException if the sequencing is interrupted before it finishes, e.g.,
+ because a parameter value could not be evaluated
+ all Exceptions possibly raised during sequencing
+ """
+ from matplotlib import pyplot as plt
+
+ try:
+ program = pulse.create_program(parameters=parameters)
+ except AttributeError:
+ program = pulse
+
+ if sample_rate is None:
+ if time_slice is None:
+ duration = program.duration
+ else:
+ duration = time_slice[1]-time_slice[0]
+ if duration == 0:
+ sample_rate = 1
+ else:
+ duration_per_sample = float(duration) / 1000
+ sample_rate = 1 / duration_per_sample
+
+ if program is not None:
+ times, voltages, measurements = render(program,
+ sample_rate,
+ render_measurements=bool(plot_measurements),
+ time_slice=time_slice)
+ else:
+ times, voltages, measurements = np.array([]), dict(), []
+
+ duration = 0
+ if times.size == 0:
+ warnings.warn("Pulse to be plotted is empty!")
+ elif times.size > maximum_points:
+ # todo [2018-05-30]: since it results in an empty return value this should arguably be an exception, not just a warning
+ warnings.warn(f"Sampled pulse of size {times.size} is lager than {maximum_points}",
+ stacklevel=2)
+ return None
+ else:
+ duration = times[-1]
+
+ if time_slice is None:
+ time_slice = (0, duration)
+
+ legend_handles = []
+ if axes is None:
+ # plot to figure
+ figure = plt.figure()
+ axes = figure.add_subplot(111)
+
+ if plot_channels is not None:
+ voltages = {ch: voltage
+ for ch, voltage in voltages.items()
+ if ch in plot_channels}
+
+ for ch_name, voltage in voltages.items():
+ label = 'channel {}'.format(ch_name)
+ if stepped:
+ line, = axes.step(times, voltage, **{**dict(where='post', label=label), **kwargs})
+ else:
+ line, = axes.plot(times, voltage, **{**dict(label=label), **kwargs})
+ legend_handles.append(line)
+
+ if plot_measurements:
+ measurement_dict = dict()
+ for name, begin, length in measurements:
+ if name in plot_measurements:
+ measurement_dict.setdefault(name, []).append((begin, begin+length))
+
+ color_map = get_cmap('plasma')
+ meas_colors = {name: color_map(i/len(measurement_dict))
+ for i, name in enumerate(measurement_dict.keys())}
+ for name, begin_end_list in measurement_dict.items():
+ for begin, end in begin_end_list:
+ poly = axes.axvspan(begin, end, alpha=0.2, label=name, edgecolor='black', facecolor=meas_colors[name])
+ legend_handles.append(poly)
+
+ axes.legend(handles=legend_handles)
+
+ max_voltage = max((max(channel, default=0) for channel in voltages.values()), default=0)
+ min_voltage = min((min(channel, default=0) for channel in voltages.values()), default=0)
+
+ # add some margins in the presentation
+ axes.set_xlim(-0.5+time_slice[0], time_slice[1] + 0.5)
+ voltage_difference = max_voltage-min_voltage
+ if voltage_difference>0:
+ axes.set_ylim(min_voltage - 0.1*voltage_difference, max_voltage + 0.1*voltage_difference)
+ axes.set_xlabel('Time (ns)')
+ axes.set_ylabel('Voltage (a.u.)')
+
+ if pulse.identifier:
+ axes.set_title(pulse.identifier)
+
+ if show:
+ with warnings.catch_warnings():
+ # do not show warnings in jupyter notebook with matplotlib inline backend
+ warnings.filterwarnings(action="ignore",message=".*which is a non-GUI backend, so cannot show the figure.*")
+ warnings.filterwarnings(action="ignore",message=".*is non-interactive, and thus cannot be shown.*")
+ axes.get_figure().show()
+ return axes.get_figure()
+
+
+@functools.singledispatch
+def plot_2d(program: Loop, channels: Tuple[ChannelID, ChannelID],
+ sample_rate: float = None,
+ ax: plt.Axes = None,
+ plot_kwargs: Mapping = None) -> plt.Figure:
+ """Plot the pulse/program in the plane of the given channels.
+
+ Args:
+ program: The program to plot
+ channels: (x_axis, y_axis) name tuple
+ sample_rate: Sample rate to use. Defaults to max(1000 samples per program, 10 per nano second)
+ ax: Axis to plot into.
+ plot_kwargs: Forwarded to the plot function.
+ """
+ if sample_rate is None:
+ sample_rate = max(1000 / program.duration, 10)
+
+ _, rendered, _ = render(program, sample_rate, plot_channels=set(channels))
+ x_y = np.array([rendered[channels[0]], rendered[channels[1]]])
+ keep = np.full(x_y.shape[1], fill_value=True)
+ keep[1:] = np.any(x_y[:, 1:] != x_y[:, :-1], axis=0)
+ x_y_plt = x_y[:, keep]
+
+ ax = ax or plt.subplots()[1]
+ ax.plot(x_y_plt[0, :], x_y_plt[1, :], **(plot_kwargs or {}))
+ ax.set_xlabel(channels[0])
+ ax.set_ylabel(channels[1])
+ return ax.get_figure()
+
+
+@plot_2d.register
+def _(pulse_template: PulseTemplate,
+ channels: Tuple[ChannelID, ChannelID],
+ sample_rate: float = None,
+ ax: plt.Axes = None,
+ plot_kwargs: Mapping = None,
+ parameters=None,
+ channel_mapping=None) -> plt.Figure:
+
+ if channel_mapping is None:
+ channel_mapping = {ch: ch if ch in channels else None
+ for ch in pulse_template.defined_channels}
+ create_program_kwargs = {'channel_mapping': channel_mapping}
+ if parameters is not None:
+ create_program_kwargs['parameters'] = parameters
+
+ program = pulse_template.create_program(**create_program_kwargs)
+ return plot_2d(program, channels, sample_rate=sample_rate, ax=ax, plot_kwargs=plot_kwargs)
+
+
+class PlottingNotPossibleException(Exception):
+ """Indicates that plotting is not possible because the sequencing process did not translate
+ the entire given PulseTemplate structure."""
+
+ def __init__(self, pulse, description = None) -> None:
+ super().__init__()
+ self.pulse = pulse
+ self.description = description
+ def __str__(self) -> str:
+ if self.description is None:
+ return "Plotting is not possible. There are parameters which cannot be computed."
+ else:
+ return "Plotting is not possible: %s." % self.description
+
+
diff --git a/qupulse/program/__init__.py b/qupulse/program/__init__.py
new file mode 100644
index 000000000..c96d7fd78
--- /dev/null
+++ b/qupulse/program/__init__.py
@@ -0,0 +1,47 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+"""This package contains the means to construct a program from a pulse template.
+
+A program is an un-parameterized multichannel time to voltage relation. They are constructed by sequencing playback
+commands which typically mean that an arbitrary waveform is played.
+
+The arbitrary waveforms are defined in the :py:mod:`.waveforms` module.
+
+:py:mod:`.transformation` contains useful transformations for waveforms which for example allow the
+construction of virtual channels, i.e. linear combinations of channels from a set of other channes.
+
+:py:mod:`.loop` contains the legacy program representation with is an aribtrariliy nested sequence/repetition structure
+of waveform playbacks.
+
+:py:mod:`.linspace` contains a more modern program representation to efficiently execute linearly spaced voltage sweeps
+even if interleaved with constant segments.
+
+:py:mod:`.volatile` contains the machinery to declare quickly changable program parameters. This functionality is stale
+and was not used by the library authors for a long term. It is very useful for dynamic nuclear polarization which is not
+used/required/possible with (purified) silicon samples.
+"""
+
+from qupulse.program.protocol import Program, ProgramBuilder
+from qupulse.program.values import DynamicLinearValue, HardwareTime, \
+ HardwareVoltage, RepetitionCount
+from qupulse.program.waveforms import Waveform
+from qupulse.program.transformation import Transformation
+from qupulse.program.volatile import VolatileRepetitionCount
+
+
+# backwards compatibility
+# DEPRECATED but writing warning code for this is too complex
+SimpleExpression = DynamicLinearValue
+
+
+def default_program_builder() -> ProgramBuilder:
+ """This function returns an instance of the default program builder class :class:`.LoopBuilder` in the default
+ configuration.
+
+ Returns:
+ A program builder instance.
+ """
+ from qupulse.program.loop import LoopBuilder
+ return LoopBuilder()
diff --git a/qupulse/program/linspace.py b/qupulse/program/linspace.py
new file mode 100644
index 000000000..c610ff988
--- /dev/null
+++ b/qupulse/program/linspace.py
@@ -0,0 +1,692 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+import contextlib
+import dataclasses
+import warnings
+
+import numpy as np
+import math
+import copy
+
+from dataclasses import dataclass
+from abc import ABC, abstractmethod
+from typing import Mapping, Optional, Sequence, ContextManager, Iterable, Tuple, \
+ Union, Dict, List, Set, ClassVar, Callable, Any, AbstractSet
+from collections import OrderedDict
+
+from qupulse import ChannelID, MeasurementWindow
+from qupulse.parameter_scope import Scope, MappedScope, FrozenDict
+from qupulse.program.protocol import (ProgramBuilder, Waveform, BaseProgramBuilder, Program, )
+from qupulse.program.values import RepetitionCount, HardwareTime, HardwareVoltage, DynamicLinearValue, TimeType
+from qupulse.program.volatile import VolatileRepetitionCount, InefficientVolatility
+from qupulse.program.waveforms import TransformingWaveform
+
+# this resolution is used to unify increments
+# the increments themselves remain floats
+DEFAULT_INCREMENT_RESOLUTION: float = 1e-9
+
+
+@dataclass(frozen=True)
+class DepKey:
+ """The key that identifies how a certain set command depends on iteration indices. The factors are rounded with a
+ given resolution to be independent on rounding errors.
+
+ These objects allow backends which support it to track multiple amplitudes at once.
+ """
+ factors: Tuple[int, ...]
+
+ @classmethod
+ def from_voltages(cls, voltages: Sequence[float], resolution: float):
+ # remove trailing zeros
+ while voltages and voltages[-1] == 0:
+ voltages = voltages[:-1]
+ return cls(tuple(int(round(voltage / resolution)) for voltage in voltages))
+
+
+@dataclass
+class LinSpaceNode(ABC):
+ """AST node for a program that supports linear spacing of set points as well as nested sequencing and repetitions"""
+
+ @abstractmethod
+ def dependencies(self) -> Mapping[int, set[Tuple[float, ...]]]:
+ """Returns a mapping from channel indices to the iteration indices dependencies that those channels have inside
+ this node.
+
+ Returns:
+ Mapping from channel indices to the iteration indices dependencies
+ """
+ raise NotImplementedError
+
+ def reversed(self, offset: int, lengths: list):
+ """Get the time reversed version of this linspace node. Since this is a non-local operation the arguments give
+ the context.
+
+ Args:
+ offset: Active iterations that are not reserved
+ lengths: Lengths of the currently active iterations that have to be reversed
+
+ Returns:
+ Time reversed version.
+ """
+ raise NotImplementedError
+
+
+@dataclass
+class LinSpaceHold(LinSpaceNode):
+ """Hold voltages for a given time. The voltages and the time may depend on the iteration index."""
+
+ bases: Tuple[float, ...]
+ factors: Tuple[Optional[Tuple[float, ...]], ...]
+
+ duration_base: TimeType
+ duration_factors: Optional[Tuple[TimeType, ...]]
+
+ def dependencies(self) -> Mapping[int, set]:
+ return {idx: {factors}
+ for idx, factors in enumerate(self.factors)
+ if factors}
+
+ def reversed(self, offset: int, lengths: list):
+ if not lengths:
+ return self
+ # If the iteration length is `n`, the starting point is shifted by `n - 1`
+ steps = [length - 1 for length in lengths]
+ bases = []
+ factors = []
+ for ch_base, ch_factors in zip(self.bases, self.factors):
+ if ch_factors is None or len(ch_factors) <= offset:
+ bases.append(ch_base)
+ factors.append(ch_factors)
+ else:
+ ch_reverse_base = ch_base + sum(step * factor
+ for factor, step in zip(ch_factors[offset:], steps))
+ reversed_factors = ch_factors[:offset] + tuple(-f for f in ch_factors[offset:])
+ bases.append(ch_reverse_base)
+ factors.append(reversed_factors)
+
+ if self.duration_factors is None or len(self.duration_factors) <= offset:
+ duration_factors = self.duration_factors
+ duration_base = self.duration_base
+ else:
+ duration_base = self.duration_base + sum((step * factor
+ for factor, step in zip(self.duration_factors[offset:], steps)), TimeType(0))
+ duration_factors = self.duration_factors[:offset] + tuple(-f for f in self.duration_factors[offset:])
+ return LinSpaceHold(tuple(bases), tuple(factors), duration_base=duration_base, duration_factors=duration_factors)
+
+
+@dataclass
+class LinSpaceArbitraryWaveform(LinSpaceNode):
+ """This is just a wrapper to pipe arbitrary waveforms through the system."""
+ waveform: Waveform
+ channels: Tuple[ChannelID, ...]
+
+ def dependencies(self) -> Mapping[int, set[Tuple[float, ...]]]:
+ return {}
+
+ def reversed(self, offset: int, lengths: list):
+ return LinSpaceArbitraryWaveform(
+ waveform=self.waveform.reversed(),
+ channels=self.channels,
+ )
+
+
+@dataclass
+class LinSpaceRepeat(LinSpaceNode):
+ """Repeat the body count times."""
+ body: Tuple[LinSpaceNode, ...]
+ count: int
+
+ def dependencies(self):
+ dependencies = {}
+ for node in self.body:
+ for idx, deps in node.dependencies().items():
+ dependencies.setdefault(idx, set()).update(deps)
+ return dependencies
+
+ def reversed(self, offset: int, counts: list):
+ return LinSpaceRepeat(tuple(node.reversed(offset, counts) for node in reversed(self.body)), self.count)
+
+
+@dataclass
+class LinSpaceIter(LinSpaceNode):
+ """Iteration in linear space are restricted to range 0 to length.
+
+ Offsets and spacing are stored in the hold node."""
+ body: Tuple[LinSpaceNode, ...]
+ length: int
+
+ def dependencies(self):
+ dependencies = {}
+ for node in self.body:
+ for idx, deps in node.dependencies().items():
+ # remove the last elemt in index because this iteration sets it -> no external dependency
+ shortened = {dep[:-1] for dep in deps}
+ if shortened != {()}:
+ dependencies.setdefault(idx, set()).update(shortened)
+ return dependencies
+
+ def reversed(self, offset: int, lengths: list):
+ lengths.append(self.length)
+ reversed_iter = LinSpaceIter(tuple(node.reversed(offset, lengths) for node in reversed(self.body)), self.length)
+ lengths.pop()
+ return reversed_iter
+
+
+class LinSpaceBuilder(BaseProgramBuilder):
+ """This program builder supports efficient translation of pulse templates that use symbolic linearly
+ spaced voltages and durations.
+
+ The channel identifiers are reduced to their index in the given channel tuple.
+
+ Arbitrary waveforms are not implemented yet
+ """
+
+ def __init__(self, channels: Tuple[ChannelID, ...]):
+ super().__init__()
+ self._name_to_idx = {name: idx for idx, name in enumerate(channels)}
+ self._idx_to_name = channels
+
+ self._stack = [[]]
+ self._ranges = []
+
+ def _root(self):
+ return self._stack[0]
+
+ def _get_rng(self, idx_name: str) -> range:
+ return self._get_ranges()[idx_name]
+
+ def inner_scope(self, scope: Scope) -> Scope:
+ """This function is necessary to inject program builder specific parameter implementations into the build
+ process."""
+ if self._ranges:
+ name, _ = self._ranges[-1]
+ return scope.overwrite({name: DynamicLinearValue(base=0, factors={name: 1})})
+ else:
+ return scope
+
+ def _get_ranges(self):
+ return dict(self._ranges)
+
+ def _transformed_hold_voltage(self, duration: HardwareTime, voltages: Mapping[ChannelID, HardwareVoltage]):
+ voltages = sorted((self._name_to_idx[ch_name], value) for ch_name, value in voltages.items())
+ voltages = [value for _, value in voltages]
+
+ ranges = self._get_ranges()
+ factors = []
+ bases = []
+ for value in voltages:
+ if isinstance(value, float):
+ bases.append(value)
+ factors.append(None)
+ continue
+ offsets = value.factors
+ base = value.base
+ incs = []
+ for rng_name, rng in ranges.items():
+ start = 0.
+ step = 0.
+ offset = offsets.get(rng_name, None)
+ if offset:
+ start += rng.start * offset
+ step += rng.step * offset
+ base += start
+ incs.append(step)
+ factors.append(tuple(incs))
+ bases.append(base)
+
+ if isinstance(duration, DynamicLinearValue):
+ duration_factors = duration.factors
+ duration_base = duration.base
+ else:
+ duration_base = duration
+ duration_factors = None
+
+ set_cmd = LinSpaceHold(bases=tuple(bases),
+ factors=tuple(factors),
+ duration_base=duration_base,
+ duration_factors=duration_factors)
+
+ self._stack[-1].append(set_cmd)
+
+ def _transformed_play_arbitrary_waveform(self, waveform: Waveform):
+ return self._stack[-1].append(LinSpaceArbitraryWaveform(waveform, self._idx_to_name))
+
+ def measure(self, measurements: Optional[Sequence[MeasurementWindow]]):
+ """Ignores measurements"""
+ pass
+
+ def with_repetition(self, repetition_count: RepetitionCount,
+ measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']:
+ if repetition_count == 0:
+ return
+ if isinstance(repetition_count, VolatileRepetitionCount):
+ warnings.warn(f"{type(self).__name__} does not support volatile repetition counts.",
+ category=InefficientVolatility)
+
+ self._stack.append([])
+ yield self
+ blocks = self._stack.pop()
+ if blocks:
+ self._stack[-1].append(LinSpaceRepeat(body=tuple(blocks), count=repetition_count))
+
+ @contextlib.contextmanager
+ def with_sequence(self,
+ measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']:
+ yield self
+
+ def new_subprogram(self, global_transformation: 'Transformation' = None) -> ContextManager['ProgramBuilder']:
+ raise NotImplementedError('Not implemented yet (postponed)')
+
+ def with_iteration(self, index_name: str, rng: range,
+ measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']:
+ if len(rng) == 0:
+ return
+ self._stack.append([])
+ self._ranges.append((index_name, rng))
+ scope = self.build_context.scope.overwrite({index_name: DynamicLinearValue(base=0, factors={index_name: 1})})
+ with self._with_patched_context(scope=scope):
+ yield self
+ cmds = self._stack.pop()
+ self._ranges.pop()
+ if cmds:
+ self._stack[-1].append(LinSpaceIter(body=tuple(cmds), length=len(rng)))
+
+ @contextlib.contextmanager
+ def time_reversed(self) -> Iterable['LinSpaceBuilder']:
+ self._stack.append([])
+ yield self
+ inner = self._stack.pop()
+ offset = len(self._ranges)
+ self._stack[-1].extend(node.reversed(offset, []) for node in reversed(inner))
+
+ def to_program(self) -> Optional['LinSpaceProgram']:
+ if root := self._root():
+ return LinSpaceProgram(
+ root=tuple(root),
+ defined_channels=self._idx_to_name,
+ )
+ else:
+ return None
+
+
+@dataclass
+class LoopLabel:
+ idx: int
+ count: int
+
+
+@dataclass
+class Increment:
+ channel: int
+ value: float
+ dependency_key: DepKey
+
+
+@dataclass
+class Set:
+ channel: int
+ value: float
+ key: DepKey = dataclasses.field(default_factory=lambda: DepKey(()))
+
+
+@dataclass
+class Wait:
+ duration: TimeType
+
+
+@dataclass
+class LoopJmp:
+ idx: int
+
+
+@dataclass
+class Play:
+ waveform: Waveform
+ channels: Tuple[ChannelID]
+
+
+Command = Union[Increment, Set, LoopLabel, LoopJmp, Wait, Play]
+
+
+@dataclass(frozen=True)
+class DepState:
+ base: float
+ iterations: Tuple[int, ...]
+
+ def required_increment_from(self, previous: 'DepState', factors: Sequence[float]) -> float:
+ """Calculate the required increment from the previous state to the current given the factors that determine
+ the voltage dependency of each index.
+
+ By convention there are only two possible values for each iteration index integer in self: 0 or the last index
+ The three possible increments for each iteration are none, regular and jump to next line.
+
+ The previous dependency state can have a different iteration length if the trailing factors now or during the
+ last iteration are zero.
+
+ Args:
+ previous: The previous state to calculate the required increment from. It has to belong to the same DepKey.
+ factors: The number of factors has to be the same as the current number of iterations.
+
+ Returns:
+ The increment
+ """
+ assert len(self.iterations) == len(factors)
+
+ increment = self.base - previous.base
+ for old, new, factor in zip(previous.iterations, self.iterations, factors):
+ # By convention there are only two possible values for each integer here: 0 or the last index
+ # The three possible increments are none, regular and jump to next line
+
+ if old == new:
+ # we are still in the same iteration of this sweep
+ pass
+
+ elif old < new:
+ assert old == 0
+ # regular iteration, although the new value will probably be > 1, the resulting increment will be
+ # applied multiple times so only one factor is needed.
+ increment += factor
+
+ else:
+ assert new == 0
+ # we need to jump back. The old value gives us the number of increments to reverse
+ increment -= factor * old
+ return increment
+
+
+@dataclass
+class _TranslationState:
+ """This is the state of a translation of a LinSpace program to a command sequence."""
+
+ label_num: int = dataclasses.field(default=0)
+ commands: List[Command] = dataclasses.field(default_factory=list)
+ iterations: List[int] = dataclasses.field(default_factory=list)
+ active_dep: Dict[int, DepKey] = dataclasses.field(default_factory=dict)
+ dep_states: Dict[int, Dict[DepKey, DepState]] = dataclasses.field(default_factory=dict)
+ plain_voltage: Dict[int, float] = dataclasses.field(default_factory=dict)
+ resolution: float = dataclasses.field(default_factory=lambda: DEFAULT_INCREMENT_RESOLUTION)
+
+ def new_loop(self, count: int):
+ label = LoopLabel(self.label_num, count)
+ jmp = LoopJmp(self.label_num)
+ self.label_num += 1
+ return label, jmp
+
+ def get_dependency_state(self, dependencies: Mapping[int, set]):
+ return {
+ self.dep_states.get(ch, {}).get(DepKey.from_voltages(dep, self.resolution), None)
+ for ch, deps in dependencies.items()
+ for dep in deps
+ }
+
+ def set_voltage(self, channel: int, value: float):
+ key = DepKey(())
+ if self.active_dep.get(channel, None) != key or self.plain_voltage.get(channel, None) != value:
+ self.commands.append(Set(channel, value, key))
+ self.active_dep[channel] = key
+ self.plain_voltage[channel] = value
+
+ def _add_repetition_node(self, node: LinSpaceRepeat):
+ pre_dep_state = self.get_dependency_state(node.dependencies())
+ label, jmp = self.new_loop(node.count)
+ initial_position = len(self.commands)
+ self.commands.append(label)
+ self.add_node(node.body)
+ post_dep_state = self.get_dependency_state(node.dependencies())
+ if pre_dep_state != post_dep_state:
+ # hackedy
+ self.commands.pop(initial_position)
+ self.commands.append(label)
+ label.count -= 1
+ self.add_node(node.body)
+ self.commands.append(jmp)
+
+ def _add_iteration_node(self, node: LinSpaceIter):
+ self.iterations.append(0)
+ self.add_node(node.body)
+
+ if node.length > 1:
+ self.iterations[-1] = node.length - 1
+ label, jmp = self.new_loop(node.length - 1)
+ self.commands.append(label)
+ self.add_node(node.body)
+ self.commands.append(jmp)
+ self.iterations.pop()
+
+ def _set_indexed_voltage(self, channel: int, base: float, factors: Sequence[float]):
+ dep_key = DepKey.from_voltages(voltages=factors, resolution=self.resolution)
+ new_dep_state = DepState(
+ base,
+ iterations=tuple(self.iterations)
+ )
+
+ current_dep_state = self.dep_states.setdefault(channel, {}).get(dep_key, None)
+ if current_dep_state is None:
+ assert all(it == 0 for it in self.iterations)
+ self.commands.append(Set(channel, base, dep_key))
+ self.active_dep[channel] = dep_key
+
+ else:
+ inc = new_dep_state.required_increment_from(previous=current_dep_state, factors=factors)
+
+ # we insert all inc here (also inc == 0) because it signals to activate this amplitude register
+ if inc or self.active_dep.get(channel, None) != dep_key:
+ self.commands.append(Increment(channel, inc, dep_key))
+ self.active_dep[channel] = dep_key
+ self.dep_states[channel][dep_key] = new_dep_state
+
+ def _add_hold_node(self, node: LinSpaceHold):
+ if node.duration_factors:
+ raise NotImplementedError("TODO")
+
+ for ch, (base, factors) in enumerate(zip(node.bases, node.factors)):
+ if factors is None:
+ self.set_voltage(ch, base)
+ continue
+
+ else:
+ self._set_indexed_voltage(ch, base, factors)
+
+ self.commands.append(Wait(node.duration_base))
+
+ def add_node(self, node: Union[LinSpaceNode, Sequence[LinSpaceNode]]):
+ """Translate a (sequence of) linspace node(s) to commands and add it to the internal command list."""
+ if isinstance(node, Sequence):
+ for lin_node in node:
+ self.add_node(lin_node)
+
+ elif isinstance(node, LinSpaceRepeat):
+ self._add_repetition_node(node)
+
+ elif isinstance(node, LinSpaceIter):
+ self._add_iteration_node(node)
+
+ elif isinstance(node, LinSpaceHold):
+ self._add_hold_node(node)
+
+ elif isinstance(node, LinSpaceArbitraryWaveform):
+ self.commands.append(Play(node.waveform, node.channels))
+
+ else:
+ raise TypeError("The node type is not handled", type(node), node)
+
+
+def to_increment_commands(linspace_nodes: 'LinSpaceProgram') -> List[Command]:
+ """translate the given linspace node tree to a minimal sequence of set and increment commands as well as loops."""
+ state = _TranslationState()
+ state.add_node(linspace_nodes.root)
+ return state.commands
+
+
+def transform_linspace_commands(command_list: List[Command],
+ channel_transformations: Mapping[ChannelID, 'ChannelTransformation'],
+ ) -> List[Command]:
+ # all commands = Union[Increment, Set, LoopLabel, LoopJmp, Wait, Play]
+ trafos_by_channel_idx = list(channel_transformations.values())
+
+ for command in command_list:
+ if isinstance(command, (LoopLabel, LoopJmp, Play, Wait)):
+ # play is handled by transforming the sampled waveform
+ continue
+ elif isinstance(command, Increment):
+ ch_trafo = trafos_by_channel_idx[command.channel]
+ if ch_trafo.voltage_transformation:
+ raise RuntimeError("Cannot apply a voltage transformation to a linspace increment command")
+ command.value /= ch_trafo.amplitude
+ elif isinstance(command, Set):
+ ch_trafo = trafos_by_channel_idx[command.channel]
+ if ch_trafo.voltage_transformation:
+ command.value = float(ch_trafo.voltage_transformation(command.value))
+ command.value -= ch_trafo.offset
+ command.value /= ch_trafo.amplitude
+ else:
+ raise NotImplementedError(command)
+
+ return command_list
+
+
+def _get_waveforms_dict(transformed_commands: Sequence[Command]) -> Mapping[Waveform,Any]:
+ return OrderedDict((command.waveform, None)
+ for command in transformed_commands if isinstance(command,Play))
+
+
+@dataclass
+class LinSpaceProgram(Program):
+ root: Tuple[LinSpaceNode, ...]
+ defined_channels: Tuple[ChannelID, ...]
+
+ @property
+ def duration(self) -> TimeType:
+ raise NotImplementedError("TODO")
+
+ def get_defined_channels(self) -> AbstractSet[ChannelID]:
+ return set(self.defined_channels)
+
+ def dependencies(self):
+ dependencies = {}
+ for node in self.root:
+ for idx, deps in node.dependencies().items():
+ dependencies.setdefault(idx, set()).update(deps)
+ return dependencies
+
+ def get_waveforms_dict(self,
+ channels: Sequence[ChannelID], #!!! this argument currently does not do anything.
+ channel_transformations: Mapping[ChannelID,'ChannelTransformation'],
+ ) -> Mapping[Waveform,Any]:
+ commands = to_increment_commands(self)
+ commands_transformed = transform_linspace_commands(commands,channel_transformations)
+ return _get_waveforms_dict(commands_transformed)
+
+
+class LinSpaceVM:
+ def __init__(self, channels: int,
+ sample_resolution: TimeType = TimeType.from_fraction(1, 2)):
+ self.current_values = [np.nan] * channels
+ self.sample_resolution = sample_resolution
+ self.time = TimeType(0)
+ self.registers = tuple({} for _ in range(channels))
+
+ self.history: List[Tuple[TimeType, List[float]]] = []
+
+ self.commands = None
+ self.label_targets = None
+ self.label_counts = None
+ self.current_command = None
+
+ def _play_arbitrary(self, play: Play):
+ """Play an arbitrary waveform.
+
+ This implementation samples the waveform with self.sample_resolution. We reinterpret this as a sequence of
+ Set and Hold commands.
+
+ Args:
+ play: The waveform to play
+ """
+ start_time = copy.copy(self.time)
+
+ # we do arbitrary time resolution sampling in a single batch
+ dt = self.sample_resolution
+ total_duration = play.waveform.duration
+ # we ceil, because we need to cover the complete duration. The last sample can have a shorter duration
+ n_samples = math.ceil(total_duration / dt)
+ exact_times = [dt * n for n in range(n_samples)]
+ sample_times = np.array(exact_times, dtype=np.float64)
+ samples = []
+ for ch in play.channels:
+ samples.append(play.waveform.get_sampled(channel=ch, sample_times=sample_times))
+
+ end_time = self.time + total_duration
+ for values in zip(*samples):
+ # This explicitness is not efficient but desired
+ # "set" the voltages
+ self.current_values[:] = values
+
+ # "wait" for sample time or time until end
+ hold_duration = min(dt, end_time - self.time)
+ self.history.append((self.time, self.current_values.copy()))
+ self.time += hold_duration
+
+ assert self.time == start_time + total_duration
+
+ def change_state(self, cmd: Union[Set, Increment, Wait, Play]):
+ if isinstance(cmd, Play):
+ self._play_arbitrary(cmd)
+
+ elif isinstance(cmd, Wait):
+ if self.history and self.history[-1][1] == self.current_values:
+ # do not create noop entries
+ pass
+ else:
+ self.history.append(
+ (self.time, self.current_values.copy())
+ )
+ self.time += cmd.duration
+ elif isinstance(cmd, Set):
+ self.current_values[cmd.channel] = cmd.value
+ self.registers[cmd.channel][cmd.key] = cmd.value
+ elif isinstance(cmd, Increment):
+ value = self.registers[cmd.channel][cmd.dependency_key]
+ value += cmd.value
+ self.registers[cmd.channel][cmd.dependency_key] = value
+ self.current_values[cmd.channel] = value
+ else:
+ raise NotImplementedError(cmd)
+
+ def set_commands(self, commands: Sequence[Command]):
+ self.commands = []
+ self.label_targets = {}
+ self.label_counts = {}
+ self.current_command = None
+
+ for cmd in commands:
+ self.commands.append(cmd)
+ if isinstance(cmd, LoopLabel):
+ # a loop label signifies a reset count followed by the actual label that targets the following command
+ assert cmd.idx not in self.label_targets
+ self.label_targets[cmd.idx] = len(self.commands)
+
+ self.current_command = 0
+
+ def step(self):
+ cmd = self.commands[self.current_command]
+ if isinstance(cmd, LoopJmp):
+ if self.label_counts[cmd.idx] > 0:
+ self.label_counts[cmd.idx] -= 1
+ self.current_command = self.label_targets[cmd.idx]
+ else:
+ # ignore jump
+ self.current_command += 1
+ elif isinstance(cmd, LoopLabel):
+ self.label_counts[cmd.idx] = cmd.count - 1
+ self.current_command += 1
+ else:
+ self.change_state(cmd)
+ self.current_command += 1
+
+ def run(self):
+ while self.current_command < len(self.commands):
+ self.step()
+
+
diff --git a/qupulse/program/loop.py b/qupulse/program/loop.py
new file mode 100644
index 000000000..952d94c17
--- /dev/null
+++ b/qupulse/program/loop.py
@@ -0,0 +1,887 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+"""Program builder implementation that creates the legacy :py:class:`.Loop`."""
+import contextlib
+import copy
+import reprlib
+import warnings
+from collections import defaultdict, OrderedDict
+from contextlib import contextmanager
+from dataclasses import dataclass
+from enum import Enum
+from typing import Set, Union, Iterable, Optional, Sequence, Tuple, List, \
+ Generator, Mapping, cast, Dict, ContextManager, Any, AbstractSet, Iterator
+
+import numpy as np
+
+from qupulse.expressions import Expression
+from qupulse.parameter_scope import Scope, DictScope
+from qupulse.program import ProgramBuilder
+from qupulse.program.protocol import BuildContext, BuildSettings, BaseProgramBuilder
+from qupulse.program.values import RepetitionCount, HardwareTime, HardwareVoltage
+from qupulse.program.transformation import Transformation, chain_transformations
+from qupulse.program.volatile import VolatileRepetitionCount, VolatileProperty, VolatileModificationWarning
+from qupulse.program.waveforms import SequenceWaveform, RepetitionWaveform
+from qupulse.program.waveforms import TransformingWaveform
+from qupulse.program.waveforms import Waveform, ConstantWaveform
+from qupulse.pulses.metadata import TemplateMetadata
+from qupulse.pulses.range import RangeScope
+from qupulse.utils import is_integer
+from qupulse.utils.numeric import smallest_factor_ge
+from qupulse.utils.tree import Node
+from qupulse.utils.types import TimeType, MeasurementWindow, ChannelID
+
+
+__all__ = ['Loop', 'make_compatible', 'MakeCompatibleWarning', 'to_waveform', 'LoopBuilder']
+
+
+DurationStructure = Tuple[int, Union[TimeType, 'DurationStructure']]
+
+
+class Loop(Node):
+ MAX_REPR_SIZE = 2000
+ __slots__ = ('_waveform', '_measurements', '_repetition_definition', '_cached_body_duration')
+
+ """This class represents a initialized (sub-)program as a tree. Each Loop of a valid program has a repetition count
+ and either a waveform or a sequence of loops as children.
+
+ A Loop can have associated measurements which are also repeated.
+ """
+ def __init__(self,
+ parent: Union['Loop', None] = None,
+ children: Iterable['Loop'] = (),
+ waveform: Optional[Waveform] = None,
+ measurements: Optional[List[MeasurementWindow]] = None,
+ repetition_count: Union[int, VolatileRepetitionCount] = 1):
+ """Initialize a new loop
+
+ Args:
+ parent: Forwarded to Node.__init__
+ children: Forwarded to Node.__init__
+ waveform: "Payload"
+ measurements: Associated measurements
+ repetition_count: The children / waveform are repeated this often
+ """
+ super().__init__(parent=parent, children=children)
+
+ self._waveform = waveform
+ self._measurements = measurements
+ self._repetition_definition = repetition_count
+ self._cached_body_duration = None
+ assert isinstance(repetition_count, VolatileRepetitionCount) or is_integer(repetition_count)
+ assert isinstance(waveform, (type(None), Waveform))
+
+ def get_waveforms_dict(self,
+ channels: Sequence[ChannelID], #!!! this argument currently does not do anything.
+ channel_transformations: Mapping[ChannelID,'ChannelTransformation'],
+ ) -> Mapping[Waveform,Any]:
+ waveforms_dict = OrderedDict((node.waveform, None)
+ for node in self.get_depth_first_iterator() if node.is_leaf())
+ return waveforms_dict
+
+ def __eq__(self, other: 'Loop') -> bool:
+ if type(self) == type(other):
+ return (self._repetition_definition == other._repetition_definition and
+ self.waveform == other.waveform and
+ (self._measurements or None) == (other._measurements or None) and
+ len(self) == len(other) and
+ all(self_child == other_child for self_child, other_child in zip(self, other)))
+ else:
+ return NotImplemented
+
+ def append_child(self, loop: Optional['Loop'] = None, **kwargs) -> None:
+ """Append a child to this loop. Either an existing Loop object or a newly created from kwargs
+
+ Args:
+ loop: loop to append
+ **kwargs: Child is constructed with these kwargs
+
+ Raises:
+ ValueError: if called with loop and kwargs
+ """
+ if loop is not None:
+ if kwargs:
+ raise ValueError("Cannot pass a Loop object and Loop constructor arguments at the same time in "
+ "append_child")
+ arg = (loop,)
+ else:
+ arg = (kwargs,)
+ super().__setitem__(slice(len(self), len(self)), arg)
+ self._invalidate_duration(body_duration_increment=self[-1].duration)
+
+ def _invalidate_duration(self, body_duration_increment=None):
+ if self._cached_body_duration is not None:
+ if body_duration_increment is not None:
+ self._cached_body_duration += body_duration_increment
+ else:
+ self._cached_body_duration = None
+ if self.parent:
+ if body_duration_increment is not None:
+ self.parent._invalidate_duration(body_duration_increment=body_duration_increment*self.repetition_count)
+ else:
+ self.parent._invalidate_duration()
+
+ def add_measurements(self, measurements: Iterable[MeasurementWindow]):
+ """Add measurements offset by the current body duration i.e. to the END of the current loop
+
+ Args:
+ measurements: Measurements to add
+ """
+ body_duration = float(self.body_duration)
+ if body_duration == 0:
+ measurements = measurements
+ else:
+ measurements = ((mw_name, begin+body_duration, length) for mw_name, begin, length in measurements)
+
+ if self._measurements is None:
+ self._measurements = list(measurements)
+ else:
+ self._measurements.extend(measurements)
+
+ def get_defined_channels(self) -> AbstractSet[ChannelID]:
+ return next(self.get_depth_first_iterator()).waveform.defined_channels
+
+ @property
+ def waveform(self) -> Waveform:
+ return self._waveform
+
+ @waveform.setter
+ def waveform(self, val) -> None:
+ self._waveform = val
+ self._invalidate_duration()
+
+ @property
+ def body_duration(self) -> TimeType:
+ if self._cached_body_duration is None:
+ if self.is_leaf():
+ if self.waveform:
+ self._cached_body_duration = self.waveform.duration
+ else:
+ self._cached_body_duration = TimeType.from_fraction(0, 1)
+ else:
+ self._cached_body_duration = sum(child.duration for child in self)
+ return self._cached_body_duration
+
+ @property
+ def duration(self) -> TimeType:
+ return self.body_duration * TimeType.from_fraction(self.repetition_count, 1)
+
+ @property
+ def volatile_repetition(self) -> Optional[VolatileProperty]:
+ return getattr(self._repetition_definition, 'volatile_property', None)
+
+ @property
+ def repetition_definition(self) -> Union[int, VolatileRepetitionCount]:
+ return self._repetition_definition
+
+ @repetition_definition.setter
+ def repetition_definition(self, new_definition: Union[int, VolatileRepetitionCount]):
+ self._repetition_definition = new_definition
+
+ @property
+ def repetition_count(self) -> int:
+ return int(self._repetition_definition)
+
+ @repetition_count.setter
+ def repetition_count(self, val: int) -> None:
+ assert isinstance(val, (int, float))
+ new_repetition = int(val)
+ if abs(new_repetition - val) > 1e-10:
+ raise ValueError('Repetition count was not an integer')
+ self._repetition_definition = new_repetition
+
+ def unroll(self) -> None:
+ if self.is_leaf():
+ raise RuntimeError('Leaves cannot be unrolled')
+ if self.volatile_repetition:
+ warnings.warn("Unrolling a Loop with volatile repetition count", VolatileModificationWarning)
+
+ i = self.parent_index
+ self.parent[i:i+1] = (child.copy_tree_structure(new_parent=self.parent)
+ for _ in range(self.repetition_count)
+ for child in self)
+ self.parent.assert_tree_integrity()
+
+ def __setitem__(self, idx, value):
+ super().__setitem__(idx, value)
+ self._invalidate_duration()
+
+ def unroll_children(self) -> None:
+ if self.volatile_repetition:
+ warnings.warn("Unrolling a Loop with volatile repetition count", VolatileModificationWarning)
+ old_children = self.children
+ self[:] = (child.copy_tree_structure()
+ for _ in range(self.repetition_count)
+ for child in old_children)
+ self.repetition_count = 1
+ self.assert_tree_integrity()
+
+ def encapsulate(self) -> None:
+ """Add a nesting level by moving self to its children."""
+ self[:] = [Loop(children=self,
+ repetition_count=self._repetition_definition,
+ waveform=self._waveform,
+ measurements=self._measurements)]
+ self.repetition_count = 1
+ self._waveform = None
+ self._measurements = None
+ self.assert_tree_integrity()
+
+ def _get_str(self, first_prefix, other_prefixes) -> Generator[str, None, None]:
+ if self.is_leaf():
+ yield '%sEXEC %r %d times' % (first_prefix, self._waveform, self.repetition_count)
+ else:
+ yield '%sLOOP %d times:' % (first_prefix, self.repetition_count)
+
+ for elem in self:
+ yield from cast(Loop, elem)._get_str(other_prefixes + ' ->', other_prefixes + ' ')
+
+ @reprlib.recursive_repr()
+ def __str__(self) -> str:
+ str_len = 0
+ repr_list = []
+ for sub_repr in self._get_str('', ''):
+ str_len += len(sub_repr)
+
+ if self.MAX_REPR_SIZE and str_len > self.MAX_REPR_SIZE:
+ repr_list.append('...')
+ break
+ else:
+ repr_list.append(sub_repr)
+ return '\n'.join(repr_list)
+
+ @reprlib.recursive_repr()
+ def __repr__(self):
+ children = self.children
+ waveform = self.waveform
+ measurements = self._measurements
+ repetition_count = self.repetition_count
+
+ max_repr_size = self.MAX_REPR_SIZE
+
+ reprs = {}
+ if children:
+ if len(children) < max_repr_size // len('Loop(...)'):
+ reprs['children'] = repr(list(children))
+ else:
+ reprs['children'] = '[...]'
+
+ if waveform:
+ waveform_repr = repr(waveform)
+ if len(waveform_repr) >= max_repr_size:
+ waveform_repr = '...'
+ reprs['waveform'] = waveform_repr
+
+ if measurements:
+ meas_repr = repr(measurements)
+ if len(meas_repr) >= max_repr_size:
+ meas_repr = '[...]'
+ reprs['measurements'] = meas_repr
+
+ if repetition_count != 1:
+ reprs['repetition_count'] = repr(repetition_count)
+
+ kwargs = ', '.join(f'{attr}={val}' for attr, val in reprs.items())
+
+ type_name = type(self).__name__
+ max_kwargs = max(self.MAX_REPR_SIZE - len(type_name) - 2, 0)
+
+ if len(kwargs) > max_kwargs:
+ kwargs = '...'
+
+ return f'{type(self).__name__}({kwargs})'
+
+ def copy_tree_structure(self, new_parent: Union['Loop', bool]=False) -> 'Loop':
+ return type(self)(parent=self.parent if new_parent is False else new_parent,
+ waveform=self._waveform,
+ repetition_count=self._repetition_definition,
+ measurements=None if self._measurements is None else list(self._measurements),
+ children=(child.copy_tree_structure() for child in self))
+
+ def _get_measurement_windows(self, drop: bool) -> Mapping[str, np.ndarray]:
+ """Private implementation of get_measurement_windows with a slightly different data format for easier tiling.
+
+ Args:
+ drop: Drops the measurements from the Loop i.e. the Loop will no longer have measurements attached after
+ collecting them
+
+ Returns:
+ A dictionary (measurement_name -> array) with begin == array[:, 0] and length == array[:, 1]
+ """
+ temp_meas_windows = defaultdict(list)
+ if self._measurements:
+ for (mw_name, begin, length) in self._measurements:
+ temp_meas_windows[mw_name].append((begin, length))
+
+ for mw_name, begin_length_list in temp_meas_windows.items():
+ temp_meas_windows[mw_name] = [np.asarray(begin_length_list, dtype=float)]
+
+ if drop:
+ self._measurements = None
+
+ # calculate duration together with meas windows in the same iteration
+ if self.is_leaf():
+ body_duration = float(self.body_duration)
+ else:
+ offset = TimeType(0)
+ for child in self:
+ for mw_name, begins_length_array in child._get_measurement_windows(drop).items():
+ begins_length_array[:, 0] += float(offset)
+ temp_meas_windows[mw_name].append(begins_length_array)
+ offset += child.duration
+
+ body_duration = float(offset)
+
+ # formatting like this for easier debugging
+ result = {}
+
+ # repeat and add repetition based offset
+ for mw_name, begin_length_list in temp_meas_windows.items():
+ result[mw_name] = _repeat_loop_measurements(begin_length_list, self.repetition_count, body_duration)
+
+ return result
+
+ def get_measurement_windows(self, drop=False) -> Dict[str, Tuple[np.ndarray, np.ndarray]]:
+ """Iterates over all children and collect the begin and length arrays of each measurement window.
+
+ Args:
+ drop: Drops the measurements from the Loop i.e. the Loop will no longer have measurements attached after
+ collecting them.
+
+ Returns:
+ A dictionary (measurement_name -> (begin, length)) with begin and length being :class:`numpy.ndarray`
+ """
+ return {mw_name: (begin_length_list[:, 0], begin_length_list[:, 1])
+ for mw_name, begin_length_list in self._get_measurement_windows(drop=drop).items()}
+
+ def split_one_child(self, child_index=None) -> None:
+ """Take the last child that has a repetition count larger one, decrease it's repetition count and insert a copy
+ with repetition cout one after it"""
+ if child_index is not None:
+ if self[child_index].repetition_count < 2:
+ raise ValueError('Cannot split child {} as the repetition count is not larger 1')
+
+ else:
+ # we cannot reverse enumerate
+ n_child = len(self) - 1
+ for reverse_idx, child in enumerate(reversed(self)):
+ if child.repetition_count > 1:
+ forward_idx = n_child - reverse_idx
+ if not child.volatile_repetition:
+ child_index = forward_idx
+ break
+ elif child_index is None:
+ child_index = forward_idx
+ else:
+ if child_index is None:
+ raise RuntimeError('There is no child with repetition count > 1')
+
+ if self[child_index].volatile_repetition:
+ warnings.warn("Splitting a child with volatile repetition count", VolatileModificationWarning)
+
+ new_child = self[child_index].copy_tree_structure()
+ new_child.repetition_count = 1
+
+ self[child_index].repetition_count -= 1
+
+ self[child_index+1:child_index+1] = (new_child,)
+ self.assert_tree_integrity()
+
+ def flatten_and_balance(self, depth: int) -> None:
+ """Modifies the program so all tree branches have the same depth.
+
+ Args:
+ depth: Target depth of the program
+ """
+ i = 0
+ while i < len(self):
+ # only used by type checker
+ sub_program = cast(Loop, self[i])
+
+ if sub_program.depth() < depth - 1:
+ # increase nesting because the subprogram is not deep enough
+ sub_program.encapsulate()
+
+ elif not sub_program.is_balanced():
+ # balance the sub program. We revisit it in the next iteration (no change of i )
+ # because it might modify self. While writing this comment I am not sure this is true. 14.01.2020 Simon
+ sub_program.flatten_and_balance(depth - 1)
+
+ elif sub_program.depth() == depth - 1:
+ # subprogram is balanced with the correct depth
+ i += 1
+
+ elif sub_program._has_single_child_that_can_be_merged():
+ # subprogram is balanced but to deep and has no measurements -> we can "lift" the sub-sub-program
+ # TODO: There was a len(sub_sub_program) == 1 check here that I cannot explain
+ sub_program._merge_single_child()
+
+ elif not sub_program.is_leaf():
+ # subprogram is balanced but too deep
+ sub_program.unroll()
+
+ else:
+ # we land in this case if the function gets called with depth == 0 and the current subprogram is a leaf
+ i += 1
+
+ def _has_single_child_that_can_be_merged(self) -> bool:
+ """Check if self has only once child which can cheaply be merged into self by multiplying the repetition counts.
+ """
+ if len(self) == 1:
+ child = cast(Loop, self[0])
+ return not self._measurements or (child.repetition_count == 1 and not child.volatile_repetition)
+ else:
+ return False
+
+ def _merge_single_child(self):
+ """Lift the single child to current level. Requires _has_single_child_that_can_be_merged to be true"""
+ assert len(self) == 1, "bug: _merge_single_child called on loop with len != 1"
+ child = cast(Loop, self[0])
+
+ # if the child has a fixed repetition count of 1 the measurements can be merged
+ mergable_measurements = child.repetition_count == 1 and not child.volatile_repetition
+
+ assert not self._measurements or mergable_measurements, "bug: _merge_single_child called on loop with measurements"
+ assert not self._waveform, "bug: _merge_single_child called on loop with children and waveform"
+
+ measurements = child._measurements
+ if self._measurements:
+ if measurements:
+ measurements.extend(self._measurements)
+ else:
+ measurements = self._measurements
+
+ if not self.volatile_repetition and not child.volatile_repetition:
+ # simple integer multiplication
+ repetition_definition = self.repetition_count * child.repetition_count
+ elif not self.volatile_repetition:
+ repetition_definition = child._repetition_definition * self.repetition_count
+ elif not child.volatile_repetition:
+ repetition_definition = self._repetition_definition * child.repetition_count
+ else:
+ # create a new expression that depends on both
+ expression = 'parent_repetition_count * child_repetition_count'
+ repetition_definition = VolatileRepetitionCount.operation(
+ expression=expression,
+ parent_repetition_count=self._repetition_definition,
+ child_repetition_count=child._repetition_definition)
+
+ self[:] = iter(child)
+ self._waveform = child._waveform
+ self._repetition_definition = repetition_definition
+ self._measurements = measurements
+ self._invalidate_duration()
+ return True
+
+ def cleanup(self, actions=('remove_empty_loops', 'merge_single_child')):
+ """Apply the specified actions to cleanup the Loop.
+
+ remove_empty_loops: Remove loops with no children and no waveform (a DroppedMeasurementWarning is issued)
+ merge_single_child: see `_try_merge_single_child` documentation
+
+ Warnings:
+ DroppedMeasurementWarning: Likely a bug in qupulse. TODO: investigate whether there are usecases
+ """
+ if 'remove_empty_loops' in actions:
+ new_children = []
+ for child in self:
+ child = cast(Loop, child)
+ if child.is_leaf():
+ if child.waveform is None:
+ if child._measurements:
+ warnings.warn("Dropping measurement since there is no waveform attached",
+ category=DroppedMeasurementWarning)
+ else:
+ new_children.append(child)
+
+ else:
+ child.cleanup(actions)
+ if child.waveform or not child.is_leaf():
+ new_children.append(child)
+
+ elif child._measurements:
+ warnings.warn("Dropping measurement since there is no waveform in children",
+ category=DroppedMeasurementWarning)
+
+ if len(self) != len(new_children):
+ self[:] = new_children
+
+ else:
+ # only do the recursive call
+ for child in self:
+ child.cleanup(actions)
+
+ if 'merge_single_child' in actions and self._has_single_child_that_can_be_merged():
+ self._merge_single_child()
+
+ def get_duration_structure(self) -> DurationStructure:
+ """Returns a tuple that fingerprints the structure of waveform durations and repetitions of self.
+
+ One possible use case is to identify repeated duration structures and reuse the same control flow with
+ differing data.
+ """
+ if self.is_leaf():
+ return self.repetition_count, self.waveform.duration
+ else:
+ return self.repetition_count, tuple(child.get_duration_structure() for child in self)
+
+ def reverse_inplace(self):
+ if self.is_leaf():
+ self._waveform = self._waveform.reversed()
+ else:
+ self._reverse_children()
+ for child in self:
+ child.reverse_inplace()
+ if self._measurements:
+ duration = self.duration
+ self._measurements = [
+ (name, duration - (begin + length), length)
+ for name, begin, length in self._measurements
+ ]
+
+
+def to_waveform(program: Loop) -> Waveform:
+ if program.is_leaf():
+ if program.repetition_count == 1:
+ return program.waveform
+ else:
+ return RepetitionWaveform.from_repetition_count(program.waveform, program.repetition_count)
+ else:
+ if len(program) == 1:
+ sequenced_waveform = to_waveform(cast(Loop, program[0]))
+ else:
+ sequenced_waveform = SequenceWaveform.from_sequence(
+ [to_waveform(cast(Loop, sub_program))
+ for sub_program in program])
+ if program.repetition_count > 1:
+ return RepetitionWaveform.from_repetition_count(sequenced_waveform, program.repetition_count)
+ else:
+ return sequenced_waveform
+
+
+class _CompatibilityLevel(Enum):
+ compatible = 0
+ action_required = 1
+ incompatible_too_short = 2
+ incompatible_fraction = 3
+ incompatible_quantum = 4
+
+ def is_incompatible(self) -> bool:
+ return self in (self.incompatible_fraction, self.incompatible_quantum, self.incompatible_too_short)
+
+
+def _is_compatible(program: Loop, min_len: int, quantum: int, sample_rate: TimeType) -> _CompatibilityLevel:
+ """ check whether program loop is compatible with awg requirements
+ possible reasons for incompatibility:
+ program shorter than minimum length
+ program duration not an integer
+ program duration not a multiple of quantum """
+ program_duration_in_samples = program.duration * sample_rate
+
+ if program_duration_in_samples.denominator != 1:
+ return _CompatibilityLevel.incompatible_fraction
+
+ if program_duration_in_samples < min_len:
+ return _CompatibilityLevel.incompatible_too_short
+
+ if program_duration_in_samples % quantum > 0:
+ return _CompatibilityLevel.incompatible_quantum
+
+ if program.is_leaf():
+ waveform_duration_in_samples = program.body_duration * sample_rate
+ if waveform_duration_in_samples < min_len or (waveform_duration_in_samples / quantum).denominator != 1:
+ if program.volatile_repetition:
+ warnings.warn("_is_compatible requires an action which drops volatility.",
+ category=VolatileModificationWarning)
+ return _CompatibilityLevel.action_required
+ else:
+ return _CompatibilityLevel.compatible
+ else:
+ if all(_is_compatible(cast(Loop, sub_program), min_len, quantum, sample_rate) == _CompatibilityLevel.compatible
+ for sub_program in program):
+ return _CompatibilityLevel.compatible
+ else:
+ if program.volatile_repetition:
+ warnings.warn("_is_compatible requires an action which drops volatility.",
+ category=VolatileModificationWarning)
+ return _CompatibilityLevel.action_required
+
+
+def _make_compatible(program: Loop, min_len: int, quantum: int, sample_rate: TimeType) -> None:
+ if program.is_leaf():
+ program.waveform = to_waveform(program.copy_tree_structure())
+ program.repetition_count = 1
+ else:
+ comp_levels = [_is_compatible(cast(Loop, sub_program), min_len, quantum, sample_rate)
+ for sub_program in program]
+
+ if any(comp_level.is_incompatible() for comp_level in comp_levels):
+ single_run = program.duration * sample_rate / program.repetition_count
+ if (single_run / quantum).denominator == 1 and single_run >= min_len:
+ # it is enough to concatenate all children
+ new_repetition_definition = program.repetition_definition
+ program.repetition_count = 1
+ else:
+ # we need to concatenate all children and unroll
+ new_repetition_definition = 1
+
+ program.waveform = to_waveform(program.copy_tree_structure())
+ program.repetition_definition = new_repetition_definition
+ program[:] = []
+ return
+ else:
+ for sub_program, comp_level in zip(program, comp_levels):
+ if comp_level == _CompatibilityLevel.action_required:
+ _make_compatible(sub_program, min_len, quantum, sample_rate)
+
+
+def make_compatible(program: Loop, minimal_waveform_length: int, waveform_quantum: int, sample_rate: TimeType):
+ """ check program for compatibility to AWG requirements, make it compatible if necessary and possible"""
+ comp_level = _is_compatible(program,
+ min_len=minimal_waveform_length,
+ quantum=waveform_quantum,
+ sample_rate=sample_rate)
+ if comp_level == _CompatibilityLevel.incompatible_fraction:
+ raise ValueError('The program duration in samples {} is not an integer'.format(program.duration * sample_rate))
+ if comp_level == _CompatibilityLevel.incompatible_too_short:
+ raise ValueError('The program is too short to be a valid waveform. \n'
+ ' program duration in samples: {} \n'
+ ' minimal length: {}'.format(program.duration * sample_rate, minimal_waveform_length))
+ if comp_level == _CompatibilityLevel.incompatible_quantum:
+ raise ValueError('The program duration in samples {} '
+ 'is not a multiple of quantum {}'.format(program.duration * sample_rate, waveform_quantum))
+
+ elif comp_level == _CompatibilityLevel.action_required:
+ warnings.warn("qupulse will now concatenate waveforms to make the pulse/program compatible with the chosen AWG."
+ " This might take some time. If you need this pulse more often it makes sense to write it in a "
+ "way which is more AWG friendly.", MakeCompatibleWarning)
+
+ _make_compatible(program,
+ min_len=minimal_waveform_length,
+ quantum=waveform_quantum,
+ sample_rate=sample_rate)
+
+ else:
+ assert comp_level == _CompatibilityLevel.compatible
+
+
+def roll_constant_waveforms(program: Loop, minimal_waveform_quanta: int, waveform_quantum: int, sample_rate: TimeType):
+ """This function finds waveforms in program that can be replaced with repetitions of shorter waveforms and replaces
+ them. Complexity O(N_waveforms). Drops measurements because they are not correctly handled here for performance
+ reasons.
+
+ This is possible if:
+ - The waveform is constant on all channels
+ - waveform.duration * sample_rate / waveform_quantum has a factor that is bigger than minimal_waveform_quanta
+
+ Args:
+ program:
+ minimal_waveform_quanta:
+ waveform_quantum:
+ sample_rate:
+
+ Warnings:
+ DroppedMeasurementWarning: This warning is raised if a measurement is dropped.
+ """
+ if program._measurements:
+ warnings.warn("Dropping measurements. Remove measurements before calling roll_constant_waveforms by calling"
+ " get_measurement_windows(drop=True).", category=DroppedMeasurementWarning)
+ program._measurements = None
+
+ waveform = program.waveform
+
+ if waveform is None:
+ for child in program:
+ roll_constant_waveforms(child, minimal_waveform_quanta, waveform_quantum, sample_rate)
+ else:
+ waveform_quanta = (waveform.duration * sample_rate) // waveform_quantum
+
+ # example
+ # waveform_quanta = 15
+ # minimal_waveform_quanta = 2
+ # => repetition_count = 5, new_waveform_quanta = 3
+ if waveform_quanta < minimal_waveform_quanta * 2:
+ # there is no way to roll this waveform because it is too short
+ return
+
+ const_values = waveform.constant_value_dict()
+ if const_values is None:
+ # The waveform is not constant
+ return
+
+ new_waveform_quanta = smallest_factor_ge(waveform_quanta, min_factor=minimal_waveform_quanta)
+ if new_waveform_quanta == waveform_quanta:
+ # the waveform duration in samples has no suitable factor
+ # TODO: Option to insert multiple Loop objects
+ return
+
+ additional_repetition_count = waveform_quanta // new_waveform_quanta
+
+ new_waveform = ConstantWaveform.from_mapping(
+ duration=waveform_quantum * new_waveform_quanta / sample_rate,
+ constant_values=const_values)
+
+ # use the private properties to avoid invalidating the duration cache of the parent loop
+ program._repetition_definition = program.repetition_definition * additional_repetition_count
+ program._waveform = new_waveform
+
+
+def _repeat_loop_measurements(begin_length_list: List[np.ndarray],
+ repetition_count: int,
+ body_duration: float
+ ) -> np.ndarray:
+ temp_begin_length_array = np.concatenate(begin_length_list)
+
+ begin_length_array = np.tile(temp_begin_length_array, (repetition_count, 1))
+
+ shaped_begin_length_array = np.reshape(begin_length_array, (repetition_count, -1, 2))
+
+ shaped_begin_length_array[:, :, 0] += (np.arange(repetition_count) * body_duration)[:, np.newaxis]
+
+ return begin_length_array
+
+
+class MakeCompatibleWarning(ResourceWarning):
+ pass
+
+
+class DroppedMeasurementWarning(RuntimeWarning):
+ """This warning is emitted if a measurement was dropped because there was no waveform attached."""
+
+
+@dataclass
+class LoopGuard:
+ loop: Loop
+ measurements: Optional[List[MeasurementWindow]]
+
+ def append_child(self, **kwargs):
+ if self.measurements:
+ self.loop.add_measurements(self.measurements)
+ self.measurements = None
+ self.loop.append_child(**kwargs)
+
+ def add_measurements(self, measurements: List[MeasurementWindow]):
+ if self.measurements is None:
+ self.measurements = measurements
+ else:
+ self.measurements.extend(measurements)
+
+
+@dataclass
+class StackFrame:
+ loop: Union[Loop, LoopGuard]
+ iterating: Optional[Tuple[str, int]]
+
+
+class LoopBuilder(BaseProgramBuilder):
+ """
+
+ Notes fduring implementation:
+ - This program builder does not use the Loop class to generate the measurements
+
+ """
+
+ def __init__(self, initial_context: BuildContext = None, initial_settings: BuildSettings = None):
+ super().__init__(initial_context, initial_settings)
+
+ self._root: Loop = Loop()
+ self._top: Union[Loop, LoopGuard] = self._root
+
+ self._stack: List[StackFrame] = [StackFrame(self._root, None)]
+
+ def _push(self, stack_entry: StackFrame):
+ self._top = stack_entry.loop
+ self._stack.append(stack_entry)
+
+ def _pop(self):
+ stack = self._stack
+ stack.pop()
+ self._top = stack[-1].loop
+
+ def _try_append(self, loop, measurements):
+ if loop.waveform or len(loop) != 0:
+ if measurements is not None:
+ self._top.add_measurements(measurements)
+ self._top.append_child(loop=loop)
+
+ def measure(self, measurements: Optional[Sequence[MeasurementWindow]]):
+ if measurements:
+ self._top.add_measurements(measurements)
+
+ def with_repetition(self, repetition_count: RepetitionCount,
+ measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']:
+ repetition_loop = Loop(repetition_count=repetition_count)
+ self._push(StackFrame(repetition_loop, None))
+ yield self
+ self._pop()
+ self._try_append(repetition_loop, measurements)
+
+ def with_iteration(self, index_name: str, rng: range,
+ measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']:
+ with self.with_sequence(measurements):
+ top_frame = self._stack[-1]
+ context = copy.copy(self.build_context)
+ base_scope = context.scope
+ self._build_context_stack.append(context)
+ for value in rng:
+ top_frame.iterating = (index_name, value)
+ context.scope = RangeScope(base_scope, index_name, value)
+ yield self
+ self._build_context_stack.pop()
+
+ @contextmanager
+ def time_reversed(self) -> Iterator[ProgramBuilder]:
+ inner_builder = LoopBuilder(self.build_context, self.build_settings)
+ yield inner_builder
+ inner_program = inner_builder.to_program()
+
+ if inner_program:
+ inner_program.reverse_inplace()
+ self._try_append(inner_program, None)
+
+ @contextmanager
+ def with_sequence(self, measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterator[ProgramBuilder]:
+ top_frame = StackFrame(LoopGuard(self._top, measurements), None)
+ self._push(top_frame)
+ yield self
+ self._pop()
+
+ def _transformed_hold_voltage(self, duration: HardwareTime, voltages: Mapping[str, HardwareVoltage]):
+ self.play_arbitrary_waveform(ConstantWaveform.from_mapping(duration, voltages))
+
+ def _transformed_play_arbitrary_waveform(self, waveform: Waveform):
+ self._top.append_child(waveform=waveform)
+
+ @contextmanager
+ def new_subprogram(self, global_transformation: Transformation = None) -> Iterator[ProgramBuilder]:
+ inner_builder = LoopBuilder(self.build_context, self.build_settings)
+ yield inner_builder
+ inner_program = inner_builder.to_program()
+
+ if inner_program is not None:
+ measurements = [(name, begin, length)
+ for name, (begins, lengths) in inner_program.get_measurement_windows().items()
+ for begin, length in zip(begins, lengths)]
+ self._top.add_measurements(measurements)
+ waveform = to_waveform(inner_program)
+ if global_transformation is not None:
+ waveform = TransformingWaveform.from_transformation(waveform, global_transformation)
+ self.play_arbitrary_waveform(waveform)
+
+ def to_program(self) -> Optional[Loop]:
+ if len(self._stack) != 1:
+ warnings.warn("Creating program with active build stack.")
+ if self._root.waveform or len(self._root.children) != 0:
+ return self._root
+ else:
+ return None
+
+ @classmethod
+ def _testing_dummy(cls, stack):
+ builder = cls()
+ builder._stack = [StackFrame(loop, None) for loop in stack]
+ builder._root = builder._stack[0].loop
+ builder._top = builder._stack[-1].loop
+ return builder
diff --git a/qupulse/program/protocol.py b/qupulse/program/protocol.py
new file mode 100644
index 000000000..a7656a8ec
--- /dev/null
+++ b/qupulse/program/protocol.py
@@ -0,0 +1,329 @@
+"""Definition of the program builder protocol."""
+import copy
+import dataclasses
+from abc import abstractmethod, ABC
+from contextlib import contextmanager
+from typing import runtime_checkable, Protocol, Mapping, Optional, Sequence, Iterable, ContextManager, AbstractSet, \
+ Union
+
+from qupulse import MeasurementWindow
+from qupulse.expressions import Expression
+from qupulse.parameter_scope import Scope, MappedScope
+from qupulse.program.waveforms import Waveform, ConstantWaveform, TransformingWaveform
+from qupulse.program.transformation import Transformation, chain_transformations
+from qupulse.program.values import RepetitionCount, HardwareTime, HardwareVoltage
+from qupulse.pulses.metadata import TemplateMetadata
+
+from qupulse.utils.types import TimeType, ChannelID
+
+
+@runtime_checkable
+class Program(Protocol):
+ """This protocol is used to inspect and or manipulate programs. As you can see the functionality is very limited
+ because most of a program class' capability are specific to the implementation."""
+
+ @property
+ @abstractmethod
+ def duration(self) -> TimeType:
+ """The duration of the program in nanoseconds."""
+
+ @abstractmethod
+ def get_defined_channels(self) -> AbstractSet[ChannelID]:
+ """Get the set of channels that are used in this program."""
+
+
+@dataclasses.dataclass
+class BuildContext:
+ """This dataclass bundles the mutable context information during the build."""
+
+ scope: Scope = None
+ measurement_mapping: Mapping[str, Optional[str]] = None
+ channel_mapping: Mapping[ChannelID, Optional[ChannelID]] = None
+ transformation: Optional[Transformation] = None
+ minimal_sample_rate: Optional[TimeType] = None
+
+ def apply_mappings(self,
+ parameter_mapping: Mapping[str, Expression] = None,
+ measurement_mapping: Mapping[str, Optional[str]] = None,
+ channel_mapping: Mapping[ChannelID, Optional[ChannelID]] = None,
+ ) -> "BuildContext":
+ scope = self.scope
+ if parameter_mapping is not None:
+ scope = MappedScope(scope=scope, mapping=parameter_mapping)
+ mapped_measurement_mapping = self.measurement_mapping
+ if measurement_mapping is not None:
+ mapped_measurement_mapping = {k: mapped_measurement_mapping[v] for k, v in measurement_mapping.items()}
+ mapped_channel_mapping = self.channel_mapping
+ if channel_mapping is not None:
+ mapped_channel_mapping = {inner_ch: None if outer_ch is None else mapped_channel_mapping[outer_ch]
+ for inner_ch, outer_ch in channel_mapping.items()}
+ return BuildContext(scope=scope, measurement_mapping=mapped_measurement_mapping, channel_mapping=mapped_channel_mapping, transformation=self.transformation, minimal_sample_rate=self.minimal_sample_rate)
+
+
+@dataclasses.dataclass(frozen=True)
+class BuildSettings:
+ """This dataclass bundles the immutable settings."""
+ to_single_waveform: AbstractSet[str | object]
+
+
+@runtime_checkable
+class ProgramBuilder(Protocol):
+ """This protocol is used by :py:meth:`.PulseTemplate.create_program` to build a program via a variation of the
+ visitor pattern.
+
+ The pulse templates call the methods that correspond to their functionality on the program builder. For example,
+ :py:class:`.ConstantPulseTemplate` translates itself into a simple :py:meth:`.ProgramBuilder.hold_voltage` call while
+ :py:class:`SequencePulseTemplate` uses :py:meth:`.ProgramBuilder.with_sequence` to signify a logical unit with
+ attached measurements and passes the resulting object to the sequenced sub-templates.
+
+ Due to backward compatibility, the handling of measurements is a bit weird since they have to be omitted in certain
+ cases. However, this is not relevant for HDAWG specific implementations because these are expected to ignore
+ :py:meth:`.ProgramBuilder.measure` calls.
+
+ This interface makes heavy use of context managers and generators/iterators which allows for flexible iteration
+ and repetition implementation.
+ """
+
+ @property
+ @abstractmethod
+ def build_context(self) -> BuildContext:
+ """Get the current build context."""
+
+ @property
+ @abstractmethod
+ def build_settings(self) -> BuildSettings:
+ """Get the current build settings"""
+
+ @abstractmethod
+ def override(self,
+ scope: Scope = None,
+ measurement_mapping: Optional[Mapping[str, Optional[str]]] = None,
+ channel_mapping: Optional[Mapping[ChannelID, Optional[ChannelID]]] = None,
+ global_transformation: Optional[Transformation] = None,
+ to_single_waveform: AbstractSet[str | object] = None,):
+ """Override the non-None values in context and settings"""
+
+ @abstractmethod
+ def with_mappings(self, *,
+ parameter_mapping: Mapping[str, Expression],
+ measurement_mapping: Mapping[str, Optional[str]],
+ channel_mapping: Mapping[ChannelID, Optional[ChannelID]],
+ ) -> ContextManager['ProgramBuilder']:
+ """Modify the build context for the duration of the context manager.
+
+ Args:
+ parameter_mapping: A mapping of parameter names to expressions.
+ measurement_mapping: A mapping of measurement names to measurement names or None.
+ channel_mapping: A mapping of channel IDs to channel IDs or None.
+ """
+
+ @abstractmethod
+ def with_transformation(self, transformation: Transformation) -> ContextManager['ProgramBuilder']:
+ """Modify the build context for the duration of the context manager."""
+
+ @abstractmethod
+ def with_metadata(self, metadata: TemplateMetadata) -> ContextManager['ProgramBuilder']:
+ """Modify the build context for the duration of the context manager."""
+
+ @abstractmethod
+ def hold_voltage(self, duration: HardwareTime, voltages: Mapping[str, HardwareVoltage]):
+ """Hold the specified voltage for a given time. Advances the current time by ``duration``. The values are
+ hardware dependent type which are inserted into the parameter scope via :py:meth:`.ProgramBuilder.with_iteration`.
+
+ Args:
+ duration: Duration of voltage hold
+ voltages: Voltages for each channel
+ """
+
+ # further specialized commandos like play_harmonic might be added here
+
+ @abstractmethod
+ def play_arbitrary_waveform(self, waveform: Waveform):
+ """Insert the playback of an arbitrary waveform. If possible pulse templates should use more specific commands
+ like :py:meth:`.ProgramBuilder.hold_voltage` (the only more specific command at the time of this writing).
+
+ Args:
+ waveform: The waveform to play
+ """
+
+ @abstractmethod
+ def measure(self, measurements: Optional[Sequence[MeasurementWindow]]):
+ """Unconditionally add given measurements relative to the current position.
+
+ Args:
+ measurements: Measurements to add.
+ """
+
+ @abstractmethod
+ def with_repetition(self, repetition_count: RepetitionCount,
+ measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']:
+ """Start a new repetition context with given repetition count. The caller has to iterate over the return value
+ and call `:py:meth:`.ProgramBuilder.inner_scope` inside the iteration context.
+
+ Args:
+ repetition_count: Repetition count
+ measurements: These measurements are added relative to the position at the start of the iteration iff the
+ iteration is not empty.
+
+ Returns:
+ An iterable of :py:class:`ProgramBuilder` instances.
+ """
+
+ @abstractmethod
+ def with_sequence(self,
+ measurements: Optional[Sequence[MeasurementWindow]] = None) -> ContextManager['ProgramBuilder']:
+ """Start a new sequence context. The caller has to enter the returned context manager and add the sequenced
+ elements there.
+
+ Measurements that are added in to the returned program builder are discarded if the sequence is empty on exit.
+
+ Args:
+ measurements: These measurements are added relative to the position at the start of the sequence iff the
+ sequence is not empty.
+
+ Returns:
+ A context manager that returns a :py:class:`ProgramBuilder` on entering.
+ """
+
+ @abstractmethod
+ def new_subprogram(self) -> ContextManager['ProgramBuilder']:
+ """Create a context managed program builder whose contents are translated into a single waveform upon exit if
+ it is not empty.
+
+ Returns:
+ A context manager that returns a :py:class:`ProgramBuilder` on entering.
+ """
+
+ @abstractmethod
+ def with_iteration(self, index_name: str, rng: range,
+ measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']:
+ """Create an iterable that represent the body of the iteration. This can be an iterable with an element for each
+ step in the iteration or a single object that represents the complete iteration.
+
+ Args:
+ index_name: The name of index
+ rng: The range if the index
+ measurements: Measurements to add iff the iteration body is not empty.
+ """
+
+ @abstractmethod
+ def time_reversed(self) -> ContextManager['ProgramBuilder']:
+ """This returns a new context manager that will reverse everything added to it in time upon exit.
+
+ Returns:
+ A context manager that returns a :py:class:`ProgramBuilder` on entering.
+ """
+
+ @abstractmethod
+ def to_program(self) -> Optional[Program]:
+ """Generate the final program. This is allowed to invalidate the program builder.
+
+ Returns:
+ A program implementation. None if nothing was added to this program builder.
+ """
+
+
+class BaseProgramBuilder(ProgramBuilder, ABC):
+ """Helper base class for program builder to reduce code duplication. The interface is defined by :py:class:`ProgramBuilder`.
+
+ This class provides shared functionality for context and settings and correct transformation handling.
+ """
+
+ def __init__(self, initial_context: BuildContext = None, initial_settings: BuildSettings = None):
+ self._build_context_stack: list[BuildContext] = [BuildContext() if initial_context is None else initial_context]
+ self._build_settings_stack: list[BuildSettings] = [BuildSettings(set()) if initial_settings is None else initial_settings]
+
+ @property
+ def build_context(self) -> BuildContext:
+ return self._build_context_stack[-1]
+
+ @property
+ def build_settings(self) -> BuildSettings:
+ return self._build_settings_stack[-1]
+
+ def override(self,
+ scope: Scope = None,
+ measurement_mapping: Optional[Mapping[str, Optional[str]]] = None,
+ channel_mapping: Optional[Mapping[ChannelID, Optional[ChannelID]]] = None,
+ global_transformation: Optional[Transformation] = None,
+ to_single_waveform: AbstractSet[Union[str, 'PulseTemplate']] = None):
+ old_context = self._build_context_stack[-1]
+ context = BuildContext(
+ scope=old_context.scope if scope is None else scope,
+ measurement_mapping=old_context.measurement_mapping if measurement_mapping is None else measurement_mapping,
+ channel_mapping=old_context.channel_mapping if channel_mapping is None else channel_mapping,
+ transformation=old_context.transformation if global_transformation is None else global_transformation,
+ )
+ old_settings = self._build_settings_stack[-1]
+ settings = BuildSettings(
+ to_single_waveform=old_settings.to_single_waveform if to_single_waveform is None else to_single_waveform,
+ )
+
+ self._build_context_stack.append(context)
+ self._build_settings_stack.append(settings)
+
+ @contextmanager
+ def _with_patched_context(self, **kwargs):
+ context = copy.copy(self._build_context_stack[-1])
+ for name, value in kwargs.items():
+ setattr(context, name, value)
+ self._build_context_stack.append(context)
+ yield
+ self._build_context_stack.pop()
+
+ @contextmanager
+ def with_metadata(self, metadata: TemplateMetadata):
+ # metadata.to_single_waveform == "always" is handled in PulseTemplate._build_program
+ if metadata.minimal_sample_rate is not None:
+ with self._with_patched_context(minimal_sample_rate=metadata.minimal_sample_rate) as builder:
+ yield builder
+ else:
+ yield self
+
+ @contextmanager
+ def with_transformation(self, transformation: Transformation):
+ context = copy.copy(self.build_context)
+ context.transformation = chain_transformations(context.transformation, transformation)
+ self._build_context_stack.append(context)
+ yield self
+ self._build_context_stack.pop()
+
+ @contextmanager
+ def with_mappings(self, *,
+ parameter_mapping: Mapping[str, Expression],
+ measurement_mapping: Mapping[str, Optional[str]],
+ channel_mapping: Mapping[ChannelID, Optional[ChannelID]],
+ ):
+ context = self.build_context.apply_mappings(parameter_mapping, measurement_mapping, channel_mapping)
+ self._build_context_stack.append(context)
+ yield self
+ self._build_context_stack.pop()
+
+ @abstractmethod
+ def _transformed_hold_voltage(self, duration: HardwareTime, voltages: Mapping[str, HardwareVoltage]):
+ """This internal function gets the constant voltage values transformed by the current built context's transformation.
+ """
+
+ @abstractmethod
+ def _transformed_play_arbitrary_waveform(self, waveform: Waveform):
+ """This internal function gets the waveform transformed by the current built context's transformation."""
+
+ def play_arbitrary_waveform(self, waveform: Waveform):
+ transformation = self.build_context.transformation
+ if transformation:
+ transformed_waveform = TransformingWaveform(waveform, transformation)
+ self._transformed_play_arbitrary_waveform(transformed_waveform)
+ else:
+ self._transformed_play_arbitrary_waveform(waveform)
+
+ def hold_voltage(self, duration: HardwareTime, voltages: Mapping[str, HardwareVoltage]):
+ transformation = self.build_context.transformation
+ if transformation:
+ if transformation.get_constant_output_channels(voltages.keys()) != transformation.get_output_channels(voltages.keys()):
+ waveform = TransformingWaveform(ConstantWaveform.from_mapping(duration, voltages), transformation)
+ self._transformed_play_arbitrary_waveform(waveform)
+ else:
+ transformed_voltages = transformation(0.0, voltages)
+ self._transformed_hold_voltage(duration, transformed_voltages)
+ else:
+ self._transformed_hold_voltage(duration, voltages)
diff --git a/qupulse/program/transformation.py b/qupulse/program/transformation.py
new file mode 100644
index 000000000..27a2e755f
--- /dev/null
+++ b/qupulse/program/transformation.py
@@ -0,0 +1,510 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+"""Transformations to be applied to sampled waveform data (offset/scale/virtual gates)."""
+
+from typing import Any, Mapping, Set, Tuple, Sequence, AbstractSet, Union, TYPE_CHECKING, Hashable
+from abc import abstractmethod
+from numbers import Real
+import warnings
+
+import numpy as np
+
+from qupulse import ChannelID
+from qupulse.utils.types import SingletonABCMeta, frozendict, DocStringABCMeta
+from qupulse.expressions import ExpressionScalar
+from qupulse.program.values import DynamicLinearValue
+
+
+_TrafoValue = Union[Real, ExpressionScalar, DynamicLinearValue]
+
+
+__all__ = ['Transformation', 'IdentityTransformation', 'LinearTransformation', 'ScalingTransformation',
+ 'OffsetTransformation', 'ParallelChannelTransformation', 'ChainedTransformation',
+ 'chain_transformations']
+
+
+class Transformation(metaclass=DocStringABCMeta):
+ __slots__ = ()
+
+ _identity_singleton = None
+ """Transforms numeric time-voltage values for multiple channels to other time-voltage values. The number and names
+ of input and output channels might differ."""
+
+ @abstractmethod
+ def __call__(self, time: Union[np.ndarray, float],
+ data: Mapping[ChannelID, Union[np.ndarray, float]]) -> Mapping[ChannelID, Union[np.ndarray, float]]:
+ """Apply transformation to data
+ Args:
+ time:
+ data:
+
+ Returns:
+ transformed: A DataFrame that has been transformed with index == output_channels
+ """
+
+ @abstractmethod
+ def get_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
+ """Return the channel identifiers"""
+
+ @abstractmethod
+ def get_input_channels(self, output_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
+ """Channels that are required for getting data for the requested output channel"""
+
+ def chain(self, next_transformation: 'Transformation') -> 'Transformation':
+ if next_transformation is IdentityTransformation():
+ return self
+ else:
+ return chain_transformations(self, next_transformation)
+
+ def is_constant_invariant(self):
+ """Signals if the transformation always maps constants to constants."""
+ return False
+
+ def get_constant_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
+ return frozenset()
+
+ def contains_dynamic_value(self) -> bool:
+ raise NotImplementedError()
+
+
+class IdentityTransformation(Transformation, metaclass=SingletonABCMeta):
+ __slots__ = ()
+
+ def __call__(self, time: Union[np.ndarray, float],
+ data: Mapping[ChannelID, Union[np.ndarray, float]]) -> Mapping[ChannelID, Union[np.ndarray, float]]:
+ return data
+
+ def get_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
+ return input_channels
+
+ @property
+ def compare_key(self) -> type(None):
+ warnings.warn("Transformation.compare_key is deprecated since 0.11 and will be removed in 0.12",
+ DeprecationWarning, stacklevel=2)
+ return None
+
+ def __hash__(self):
+ return 0x1234991
+
+ def __eq__(self, other):
+ return self is other
+
+ def get_input_channels(self, output_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
+ return output_channels
+
+ def chain(self, next_transformation: Transformation) -> Transformation:
+ return next_transformation
+
+ def __repr__(self):
+ return 'IdentityTransformation()'
+
+ def is_constant_invariant(self):
+ """Signals if the transformation always maps constants to constants."""
+ return True
+
+ def get_constant_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
+ return input_channels
+
+ def __bool__(self):
+ return False
+
+
+class ChainedTransformation(Transformation):
+ __slots__ = ('_transformations',)
+
+ def __init__(self, *transformations: Transformation):
+ self._transformations = transformations
+
+ @property
+ def transformations(self) -> Tuple[Transformation, ...]:
+ return self._transformations
+
+ def get_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
+ for transformation in self._transformations:
+ input_channels = transformation.get_output_channels(input_channels)
+ return input_channels
+
+ def get_input_channels(self, output_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
+ for transformation in reversed(self._transformations):
+ output_channels = transformation.get_input_channels(output_channels)
+ return output_channels
+
+ def __call__(self, time: Union[np.ndarray, float],
+ data: Mapping[ChannelID, Union[np.ndarray, float]]) -> Mapping[ChannelID, Union[np.ndarray, float]]:
+ for transformation in self._transformations:
+ data = transformation(time, data)
+ return data
+
+ @property
+ def compare_key(self) -> Tuple[Transformation, ...]:
+ warnings.warn("Transformation.compare_key is deprecated since 0.11 and will be removed in 0.12",
+ DeprecationWarning, stacklevel=2)
+ return self._transformations
+
+ def __hash__(self):
+ return hash(self._transformations)
+
+ def __eq__(self, other):
+ if isinstance(other, ChainedTransformation):
+ return self._transformations == other._transformations
+ return NotImplemented
+
+ def chain(self, next_transformation) -> Transformation:
+ return chain_transformations(*self.transformations, next_transformation)
+
+ def __repr__(self):
+ return f'{type(self).__name__}{self._transformations!r}'
+
+ def is_constant_invariant(self):
+ """Signals if the transformation always maps constants to constants."""
+ return all(trafo.is_constant_invariant() for trafo in self._transformations)
+
+ def get_constant_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
+ for trafo in self._transformations:
+ input_channels = trafo.get_constant_output_channels(input_channels)
+ return input_channels
+
+
+class LinearTransformation(Transformation):
+ def __init__(self,
+ transformation_matrix: np.ndarray,
+ input_channels: Sequence[ChannelID],
+ output_channels: Sequence[ChannelID]):
+ """
+
+ Args:
+ transformation_matrix: Matrix describing the transformation with shape (output_channels, input_channels)
+ input_channels: Channel ids of the columns
+ output_channels: Channel ids of the rows
+ """
+ transformation_matrix = np.asarray(transformation_matrix)
+
+ if transformation_matrix.shape != (len(output_channels), len(input_channels)):
+ raise ValueError('Shape of transformation matrix does not match to the given channels')
+
+ output_sorter = np.argsort(output_channels)
+ transformation_matrix = transformation_matrix[output_sorter, :]
+
+ input_sorter = np.argsort(input_channels)
+ transformation_matrix = transformation_matrix[:, input_sorter]
+
+ self._matrix = transformation_matrix
+ self._input_channels = tuple(sorted(input_channels))
+ self._output_channels = tuple(sorted(output_channels))
+ self._input_channels_set = frozenset(self._input_channels)
+ self._output_channels_set = frozenset(self._output_channels)
+
+ def __call__(self, time: Union[np.ndarray, float],
+ data: Mapping[ChannelID, Union[np.ndarray, float]]) -> Mapping[ChannelID, Union[np.ndarray, float]]:
+ data_out = {forwarded_channel: data[forwarded_channel]
+ for forwarded_channel in set(data.keys()).difference(self._input_channels)}
+
+ if len(data_out) == len(data):
+ # only forwarded data
+ return data_out
+
+ try:
+ data_in = np.stack([data[in_channel] for in_channel in self._input_channels])
+ except KeyError as error:
+ raise KeyError('Invalid input channels', set(data.keys()), set(self._input_channels)) from error
+
+ transformed_data = self._matrix @ data_in
+
+ assert transformed_data.shape[0] == len(self._output_channels)
+ for out_channel, transformed_channel_data in zip(self._output_channels, transformed_data):
+ data_out[out_channel] = transformed_channel_data
+
+ return data_out
+
+ def get_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
+ if not input_channels >= self._input_channels_set:
+ # input_channels is not a superset of the required input channels
+ raise KeyError('Invalid input channels', input_channels, self._input_channels_set)
+
+ return (input_channels - self._input_channels_set) | self._output_channels_set
+
+ def get_input_channels(self, output_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
+ forwarded = output_channels - self._output_channels_set
+ if not forwarded.isdisjoint(self._input_channels):
+ raise KeyError('Is input channel', forwarded & self._input_channels_set)
+ elif output_channels.isdisjoint(self._output_channels):
+ return output_channels
+ else:
+ return forwarded | self._input_channels_set
+
+ def __hash__(self):
+ return hash((self._input_channels, self._output_channels, self._matrix.tobytes()))
+
+ def __eq__(self, other):
+ if isinstance(other, LinearTransformation):
+ return (self._input_channels == other._input_channels and
+ self._output_channels == other._output_channels and
+ np.array_equal(self._matrix, other._matrix))
+ return NotImplemented
+
+ @property
+ def compare_key(self) -> Tuple[Tuple[ChannelID], Tuple[ChannelID], bytes]:
+ warnings.warn("Transformation.compare_key is deprecated since 0.11 and will be removed in 0.12",
+ DeprecationWarning, stacklevel=2)
+ return self._input_channels, self._output_channels, self._matrix.tobytes()
+
+ def __repr__(self):
+ return ('LinearTransformation('
+ 'transformation_matrix={transformation_matrix},'
+ 'input_channels={input_channels},'
+ 'output_channels={output_channels})').format(transformation_matrix=self._matrix.tolist(),
+ input_channels=self._input_channels,
+ output_channels=self._output_channels)
+
+ def is_constant_invariant(self):
+ """Signals if the transformation always maps constants to constants."""
+ return True
+
+ def get_constant_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
+ return input_channels
+
+
+class OffsetTransformation(Transformation):
+ __slots__ = ('_offsets',)
+
+ def __init__(self, offsets: Mapping[ChannelID, _TrafoValue]):
+ """Adds an offset to each channel specified in offsets.
+
+ Channels not in offsets are forewarded
+
+ Args:
+ offsets: Channel -> offset mapping
+ """
+ self._offsets = frozendict(offsets)
+ assert _are_valid_transformation_expressions(self._offsets), f"Not valid transformation expressions: {self._offsets}"
+
+ def __call__(self, time: Union[np.ndarray, float],
+ data: Mapping[ChannelID, Union[np.ndarray, float]]) -> Mapping[ChannelID, Union[np.ndarray, float]]:
+ offsets = _instantiate_expression_dict(time, self._offsets, default_dynamic_linear_value=0.0)
+ return {channel: channel_values + offsets[channel] if channel in offsets else channel_values
+ for channel, channel_values in data.items()}
+
+ def get_input_channels(self, output_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
+ return output_channels
+
+ def get_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
+ return input_channels
+
+ def __eq__(self, other):
+ if isinstance(other, OffsetTransformation):
+ return self._offsets == other._offsets
+ return NotImplemented
+
+ def __hash__(self):
+ return hash(self._offsets)
+
+ @property
+ def compare_key(self) -> Hashable:
+ warnings.warn("Transformation.compare_key is deprecated since 0.11 and will be removed in 0.12",
+ DeprecationWarning, stacklevel=2)
+ return self._offsets
+
+ def __repr__(self):
+ return f'{type(self).__name__}({dict(self._offsets)!r})'
+
+ def is_constant_invariant(self):
+ """Signals if the transformation always maps constants to constants."""
+ return not _has_time_dependent_values(self._offsets)
+
+ def get_constant_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
+ return _get_constant_output_channels(self._offsets, input_channels)
+
+ def contains_dynamic_value(self) -> bool:
+ return any(isinstance(o,DynamicLinearValue) for o in self._offsets.values())
+
+
+class ScalingTransformation(Transformation):
+ __slots__ = ('_factors',)
+
+ def __init__(self, factors: Mapping[ChannelID, _TrafoValue]):
+ self._factors = frozendict(factors)
+ assert _are_valid_transformation_expressions(self._factors), f"Not valid transformation expressions: {self._factors}"
+
+ def __call__(self, time: Union[np.ndarray, float],
+ data: Mapping[ChannelID, Union[np.ndarray, float]]) -> Mapping[ChannelID, Union[np.ndarray, float]]:
+ factors = _instantiate_expression_dict(time, self._factors, default_dynamic_linear_value=1.0)
+ return {channel: channel_values * factors[channel] if channel in factors else channel_values
+ for channel, channel_values in data.items()}
+
+ def get_input_channels(self, output_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
+ return output_channels
+
+ def get_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
+ return input_channels
+
+ def __eq__(self, other):
+ if isinstance(other, ScalingTransformation):
+ return self._factors == other._factors
+ return NotImplemented
+
+ def __hash__(self):
+ return hash(self._factors)
+
+ @property
+ def compare_key(self) -> Hashable:
+ warnings.warn("Transformation.compare_key is deprecated since 0.11 and will be removed in 0.12",
+ DeprecationWarning, stacklevel=2)
+ return self._factors
+
+ def __repr__(self):
+ return f'{type(self).__name__}({dict(self._factors)!r})'
+
+ def is_constant_invariant(self):
+ """Signals if the transformation always maps constants to constants."""
+ return not _has_time_dependent_values(self._factors)
+
+ def get_constant_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
+ return _get_constant_output_channels(self._factors, input_channels)
+
+ def contains_dynamic_value(self) -> bool:
+ return any(isinstance(o,DynamicLinearValue) for o in self._factors.values())
+
+
+try:
+ if TYPE_CHECKING:
+ import pandas
+ PandasDataFrameType = pandas.DataFrame
+ else:
+ PandasDataFrameType = Any
+
+ def linear_transformation_from_pandas(transformation: PandasDataFrameType) -> LinearTransformation:
+ """ Creates a LinearTransformation object out of a pandas data frame.
+
+ Args:
+ transformation (pandas.DataFrame): The pandas.DataFrame object out of which a LinearTransformation will be formed.
+
+ Returns:
+ the created LinearTransformation instance
+ """
+ return LinearTransformation(transformation.values, transformation.columns, transformation.index)
+
+ LinearTransformation.from_pandas = linear_transformation_from_pandas
+except ImportError:
+ pass
+
+
+class ParallelChannelTransformation(Transformation):
+ __slots__ = ('_channels', )
+
+ def __init__(self, channels: Mapping[ChannelID, _TrafoValue]):
+ """Set channel values to given values regardless their former existence. The values can be time dependent
+ expressions.
+
+ Args:
+ channels: Channels present in this map are set to the given value.
+ """
+ self._channels: Mapping[ChannelID, _TrafoValue] = frozendict(channels.items())
+ assert _are_valid_transformation_expressions(self._channels), f"Not valid transformation expressions: {self._channels}"
+
+ def __call__(self, time: Union[np.ndarray, float],
+ data: Mapping[ChannelID, Union[np.ndarray, float]]) -> Mapping[ChannelID, Union[np.ndarray, float]]:
+ overwritten = self._instantiated_values(time)
+ return {**data, **overwritten}
+
+ def _instantiated_values(self, time):
+ scope = {'t': time}
+ array_or_float = lambda x: np.full_like(time, fill_value=x, dtype=float) if hasattr(time, '__len__') else x
+ return {channel: value.evaluate_in_scope(scope) if hasattr(value, 'evaluate_in_scope') else array_or_float(value)
+ for channel, value in self._channels.items()}
+
+ def __hash__(self):
+ return hash(self._channels)
+
+ def __eq__(self, other):
+ if isinstance(other, ParallelChannelTransformation):
+ return self._channels == other._channels
+ return NotImplemented
+
+ @property
+ def compare_key(self) -> Hashable:
+ warnings.warn("Transformation.compare_key is deprecated since 0.11 and will be removed in 0.12",
+ DeprecationWarning, stacklevel=2)
+ return self._channels
+
+ def get_input_channels(self, output_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
+ return output_channels - self._channels.keys()
+
+ def get_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
+ return input_channels | self._channels.keys()
+
+ def __repr__(self):
+ return f'{type(self).__name__}({dict(self._channels)!r})'
+
+ def is_constant_invariant(self):
+ """Signals if the transformation always maps constants to constants."""
+ return not _has_time_dependent_values(self._channels)
+
+ def get_constant_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
+ output_channels = set(input_channels)
+ for ch, value in self._channels.items():
+ if hasattr(value, 'variables'):
+ output_channels.discard(ch)
+ else:
+ output_channels.add(ch)
+
+ return output_channels
+
+ def contains_dynamic_value(self) -> bool:
+ return any(isinstance(o,DynamicLinearValue) for o in self._channels.values())
+
+
+def chain_transformations(*transformations: Transformation) -> Transformation:
+ parsed_transformations = []
+ for transformation in transformations:
+ if transformation is IdentityTransformation() or transformation is None:
+ pass
+ elif isinstance(transformation, ChainedTransformation):
+ parsed_transformations.extend(transformation.transformations)
+ else:
+ parsed_transformations.append(transformation)
+ if len(parsed_transformations) == 0:
+ return IdentityTransformation()
+ elif len(parsed_transformations) == 1:
+ return parsed_transformations[0]
+ else:
+ return ChainedTransformation(*parsed_transformations)
+
+
+def _instantiate_expression_dict(time,
+ expressions: Mapping[str, _TrafoValue],
+ default_dynamic_linear_value: Real,
+ ) -> Mapping[str, Union[Real, np.ndarray]]:
+ scope = {'t': time}
+ modified_expressions = {}
+ for name, value in expressions.items():
+ if hasattr(value, 'evaluate_in_scope'):
+ modified_expressions[name] = value.evaluate_in_scope(scope)
+ if isinstance(value, DynamicLinearValue):
+ # it is assumed that swept parameters will be handled by the ProgramBuilder accordingly
+ # such that here only an "identity" trafo is to be applied and the
+ # trafos are set in the program internally.
+ modified_expressions[name] = default_dynamic_linear_value
+ if modified_expressions:
+ return {**expressions, **modified_expressions}
+ else:
+ return expressions
+
+
+def _has_time_dependent_values(expressions: Mapping[ChannelID, _TrafoValue]) -> bool:
+ return any(hasattr(value, 'variables')
+ for value in expressions.values())
+
+
+def _get_constant_output_channels(expressions: Mapping[ChannelID, _TrafoValue],
+ constant_input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]:
+ return {ch
+ for ch in constant_input_channels
+ if not hasattr(expressions.get(ch, None), 'variables')}
+
+
+def _are_valid_transformation_expressions(expressions: Mapping[ChannelID, _TrafoValue]) -> bool:
+ return all(expr.variables == ('t',)
+ for expr in expressions.values()
+ if hasattr(expr, 'variables'))
diff --git a/qupulse/program/values.py b/qupulse/program/values.py
new file mode 100644
index 000000000..6d520b27e
--- /dev/null
+++ b/qupulse/program/values.py
@@ -0,0 +1,260 @@
+"""Runtime variable value implementations."""
+
+from dataclasses import dataclass, field
+from functools import cached_property
+from numbers import Real
+from typing import TypeVar, Generic, Mapping, Union, Tuple, Optional
+from types import MappingProxyType
+
+import numpy as np
+
+from qupulse.program.volatile import VolatileRepetitionCount
+from qupulse.utils.types import TimeType, frozendict
+from qupulse.expressions import sympy as sym_expr
+from qupulse.utils.sympy import _lambdify_modules
+
+
+NumVal = TypeVar('NumVal', bound=Real)
+
+
+@dataclass(
+ frozen=True,
+ repr=False, # dont leak frozendict implementation detail in repr
+)
+class DynamicLinearValue(Generic[NumVal]):
+ """This is a potential runtime-evaluable expression of the form
+
+ C + C1*R1 + C2*R2 + ...
+ where R1, R2, ... are potential runtime parameters.
+
+ The main use case is the expression of for loop-dependent variables where the Rs are loop indices. There the
+ expressions can be calculated via simple increments.
+
+ This class tries to pass a number and a :py:class:`sympy.expr.Expr` on best effort basis.
+ """
+
+ #: The part of this expression which is not runtime parameter-dependent
+ base: NumVal
+
+ #: A mapping of inner parameter names to the factor with which they contribute to the final value.
+ factors: Mapping[str, NumVal]
+
+ def __post_init__(self):
+ immutable = frozendict(self.factors)
+ object.__setattr__(self, 'factors', immutable)
+
+ def value(self, scope: Mapping[str, NumVal]) -> NumVal:
+ """Numeric value of the expression with the given scope.
+ Args:
+ scope: Scope in which the expression is evaluated.
+ Returns:
+ The numeric value.
+ """
+ value = self.base
+ for name, factor in self.factors.items():
+ value += scope[name] * factor
+ return value
+
+ def __abs__(self):
+ # The deifnition of an absolute value is ambiguous, but there is a case
+ # to define it as sum_i abs(f_i) + abs(base) for certain conveniences.
+ # return abs(self.base)+sum([abs(o) for o in self.factors.values()])
+ raise NotImplementedError(f'abs({self.__class__.__name__}) is ambiguous')
+
+ def __eq__(self, other):
+ if isinstance(other, type(self)):
+ return self.base == other.base and self.factors == other.factors
+
+ if (base_eq := self.base.__eq__(other)) is NotImplemented:
+ return NotImplemented
+
+ return base_eq and not self.factors
+
+ def __add__(self, other):
+ if isinstance(other, (float, int, TimeType)):
+ return DynamicLinearValue(self.base + other, self.factors)
+
+ if type(other) == type(self):
+ factors = dict(self.factors)
+ for name, value in other.factors.items():
+ factors[name] = value + factors.get(name, 0)
+ return DynamicLinearValue(self.base + other.base, factors)
+
+ # this defers evaluation when other is still a symbolic expression
+ return NotImplemented
+
+ def __radd__(self, other):
+ return self.__add__(other)
+
+ def __sub__(self, other):
+ return self.__add__(-other)
+
+ def __rsub__(self, other):
+ return (-self).__add__(other)
+
+ def __neg__(self):
+ return DynamicLinearValue(-self.base, {name: -value for name, value in self.factors.items()})
+
+ def __mul__(self, other: NumVal):
+ if isinstance(other, (float, int, TimeType)):
+ return DynamicLinearValue(self.base * other, {name: other * value for name, value in self.factors.items()})
+
+ # this defers evaluation when other is still a symbolic expression
+ return NotImplemented
+
+ def __rmul__(self, other):
+ return self.__mul__(other)
+
+ def __truediv__(self, other):
+ inv = 1 / other
+ return self.__mul__(inv)
+
+ @property
+ def free_symbols(self):
+ """This is required for the :py:class:`sympy.expr.Expr` interface compliance. Since the keys of
+ :py:attr:`.offsets` are internal parameters we do not have free symbols.
+
+ Returns:
+ An empty tuple
+ """
+ return ()
+
+ def _sympy_(self):
+ """This method is used by :py:`sympy.sympify`. This class tries to "just work" in the sympy evaluation pipelines.
+
+ Returns:
+ self
+ """
+ return self
+
+ def replace(self, r, s):
+ """We mock :class:`sympy.Expr.replace` here. This class does not support inner parameters so there is nothing
+ to replace. Importantly, the keys of the offsets are no runtime variables!
+
+ Returns:
+ self
+ """
+ return self
+
+ def __repr__(self):
+ return f"{type(self).__name__}(base={self.base!r}, factors={dict(self.factors)!r})"
+
+
+# is there any way to cast the numpy cumprod to int?
+int_type = Union[np.int64,np.int32,int]
+
+
+def _to_resolution(x, resolution):
+ """Function used by :py:class:`.ResolutionDependentValue` for rounding to resolution multiples."""
+ # to avoid conflicts between positive and negative vals from casting half to even, we only round positive numbers
+ if x < 0:
+ return -round(-x / resolution) * resolution
+ else:
+ return round(x / resolution) * resolution
+
+
+@dataclass(frozen=True)
+class ResolutionDependentValue(Generic[NumVal]):
+ """This is a potential runtime-evaluable expression of the form
+
+ o + sum_i b_i*m_i
+
+ with (potential) float o, b_i and integers m_i. o and b_i are rounded(gridded)
+ to a resolution given upon __call__.
+
+ The main use case is the correct rounding of increments used in command-based
+ voltage scans on some hardware devices, where an imprecise numeric value is
+ looped over m_i times and could, if not rounded, accumulate a proportional
+ error leading to unintended drift in output voltages when jump-back commands
+ afterwards do not account for the deviations.
+ Rounding the value preemptively and supplying corrected values to jump-back
+ commands prevents this.
+ """
+
+ bases: Tuple[NumVal, ...]
+ multiplicities: Tuple[int, ...]
+ offset: NumVal
+
+ @cached_property
+ def _is_time_or_int(self):
+ return all(isinstance(b,(TimeType,int_type)) for b in self.bases) and isinstance(self.offset,(TimeType,int_type))
+
+ def with_resolution(self, resolution: Optional[NumVal]) -> NumVal:
+ """Get the numeric value rounding to the given resolution.
+
+ Args:
+ resolution: Resolution the bases and offset are rounded to. If none all values must be integers.
+
+ Returns:
+ The rounded numeric value.
+ """
+ if resolution is None:
+ assert self._is_time_or_int
+ return sum(b * m for b, m in zip(self.bases, self.multiplicities)) + self.offset
+
+ offset = _to_resolution(self.offset, resolution)
+ base_sum = sum(_to_resolution(base, resolution) * multiplicity
+ for base, multiplicity in zip(self.bases, self.multiplicities))
+ return base_sum + offset
+
+ def __call__(self, resolution: Optional[float]) -> Union[NumVal,TimeType]:
+ """Backward compatible alias of :py:meth:`~ResolutionDependentValue.with_resolution`."""
+ return self.with_resolution(resolution)
+
+ def __bool__(self):
+ #if any value is not zero - this helps for some checks
+ return any(bool(b) for b in self.bases) or bool(self.offset)
+
+ def __add__(self, other):
+ # this should happen in the context of an offset being added to it, not the bases being modified.
+ if isinstance(other, (float, int, TimeType)):
+ return ResolutionDependentValue(self.bases, self.multiplicities, self.offset+other)
+ return NotImplemented
+
+ def __radd__(self, other):
+ return self.__add__(other)
+
+ def __sub__(self, other):
+ return self.__add__(-other)
+
+ def __mul__(self, other):
+ # this should happen when the amplitude is being scaled
+ # multiplicities are not affected
+ if isinstance(other, (float, int, TimeType)):
+ return ResolutionDependentValue(tuple(b*other for b in self.bases),self.multiplicities,self.offset*other)
+ return NotImplemented
+
+ def __rmul__(self,other):
+ return self.__mul__(other)
+
+ def __truediv__(self,other):
+ return self.__mul__(1/other)
+
+ def __float__(self):
+ return float(self.with_resolution(resolution=None))
+
+ def __str__(self):
+ return f"RDP of {sum(b*m for b,m in zip(self.bases,self.multiplicities)) + self.offset}"
+
+
+
+#This is a simple dervide class to allow better isinstance checks in the HDAWG driver
+@dataclass(frozen=True)
+class DynamicLinearValueStepped(DynamicLinearValue):
+ step_nesting_level: int
+ rng: range
+ reverse: int|bool
+
+
+# TODO: hackedy, hackedy
+sym_expr.ALLOWED_NUMERIC_SCALAR_TYPES = sym_expr.ALLOWED_NUMERIC_SCALAR_TYPES + (DynamicLinearValue,)
+
+# this keeps the simple expression in lambdified results
+_lambdify_modules.append({
+ 'DynamicLinearValue': DynamicLinearValue,
+ 'DynamicLinearValueStepped': DynamicLinearValueStepped,
+})
+
+RepetitionCount = Union[int, VolatileRepetitionCount, DynamicLinearValue[int]]
+HardwareTime = Union[TimeType, DynamicLinearValue[TimeType]]
+HardwareVoltage = Union[float, DynamicLinearValue[float]]
diff --git a/qupulse/program/volatile.py b/qupulse/program/volatile.py
new file mode 100644
index 000000000..ee80be054
--- /dev/null
+++ b/qupulse/program/volatile.py
@@ -0,0 +1,87 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+from typing import NamedTuple, Mapping
+import warnings
+import numbers
+
+
+from qupulse.parameter_scope import Scope, MappedScope, JointScope
+from qupulse.expressions import Expression, ExpressionScalar
+from qupulse.utils.types import FrozenDict, FrozenMapping
+from qupulse.utils import is_integer
+
+
+__all__ = ['VolatileProperty', 'VolatileValue', 'VolatileRepetitionCount',
+ 'InefficientVolatility', 'VolatileModificationWarning']
+
+
+VolatileProperty = NamedTuple('VolatileProperty', [('expression', Expression),
+ ('dependencies', FrozenMapping[str, Expression])])
+VolatileProperty.__doc__ = """Hashable representation of a volatile program property. It does not contain the concrete
+value. Using the dependencies attribute to calculate the value might yield unexpected results."""
+
+
+class VolatileValue:
+ """Not hashable"""
+
+ def __init__(self, expression: ExpressionScalar, scope: Scope):
+ self._expression = expression
+ self._scope = scope
+
+ @property
+ def volatile_property(self) -> VolatileProperty:
+ dependencies = self._scope.get_volatile_parameters()
+ dependencies = FrozenDict({parameter_name: dependencies[parameter_name]
+ for parameter_name in self._expression.variables
+ if parameter_name in dependencies})
+ return VolatileProperty(expression=self._expression, dependencies=dependencies)
+
+ @classmethod
+ def operation(cls, expression, **operands):
+ expression = Expression(expression)
+ assert set(expression.variables) == operands.keys()
+
+ scope = JointScope(FrozenDict(
+ {operand_name: MappedScope(operand._scope, FrozenDict({operand_name: operand._expression}))
+ for operand_name, operand in operands.items()}
+ ))
+ return cls(expression, scope)
+
+ def __sub__(self, other: int):
+ return type(self)(self._expression - other, self._scope)
+
+ def __mul__(self, other: int):
+ return type(self)(self._expression * other, self._scope)
+
+
+class VolatileRepetitionCount(VolatileValue):
+ def __int__(self):
+ value = self._expression.evaluate_in_scope(self._scope)
+ if not is_integer(value):
+ warnings.warn("Repetition count is no integer. Rounding might lead to unexpected results.")
+ value = int(round(value))
+ if value < 0:
+ warnings.warn("Repetition count is negative. Clamping lead to unexpected results.")
+ value = 0
+ return value
+
+ def update_volatile_dependencies(self, new_constants: Mapping[str, numbers.Number]) -> int:
+ self._scope = self._scope.change_constants(new_constants)
+ return int(self)
+
+ def __eq__(self, other):
+ if type(self) == type(other):
+ return self._scope is other._scope and self._expression == other._expression
+ else:
+ return NotImplemented
+
+
+class InefficientVolatility(RuntimeWarning):
+ """This warning is emitted if the requested volatility of a parameter cannot be implemented efficiently by the backend."""
+
+
+class VolatileModificationWarning(InefficientVolatility):
+ """This warning is emitted if the volatile part of a program gets modified.
+ This might imply that the volatile parameter cannot be changed anymore."""
diff --git a/qupulse/program/waveforms.py b/qupulse/program/waveforms.py
new file mode 100644
index 000000000..627bec5e0
--- /dev/null
+++ b/qupulse/program/waveforms.py
@@ -0,0 +1,1327 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+"""This module contains all waveform classes
+
+Classes:
+ - Waveform: An instantiated pulse which can be sampled to a raw voltage value array.
+"""
+
+import itertools
+import operator
+import warnings
+import dataclasses
+from abc import ABCMeta, abstractmethod
+from numbers import Real
+from typing import (
+ AbstractSet, Any, FrozenSet, Iterable, Mapping, NamedTuple, Sequence, Set,
+ Tuple, Union, cast, Optional, List, Hashable)
+from weakref import WeakValueDictionary, ref
+
+import numpy as np
+
+from qupulse import ChannelID
+from qupulse.utils.performance import is_monotonic
+from qupulse.expressions import ExpressionScalar
+from qupulse.pulses.interpolation import InterpolationStrategy
+from qupulse.utils import checked_int_cast, isclose
+from qupulse.utils.types import TimeType, time_from_float
+from qupulse.program.transformation import Transformation
+from qupulse.utils import pairwise
+
+
+class ConstantFunctionPulseTemplateWarning(UserWarning):
+ """ This warning indicates a constant waveform is constructed from a FunctionPulseTemplate """
+ pass
+
+
+__all__ = ["Waveform", "TableWaveform", "TableWaveformEntry", "FunctionWaveform", "SequenceWaveform",
+ "MultiChannelWaveform", "RepetitionWaveform", "TransformingWaveform", "ArithmeticWaveform",
+ "ConstantFunctionPulseTemplateWarning", "ConstantWaveform"]
+
+PULSE_TO_WAVEFORM_ERROR = None # error margin in pulse template to waveform conversion
+
+# these are private because there probably will be changes here
+_ALLOCATION_FUNCTION = np.full_like # pre_allocated = ALLOCATION_FUNCTION(sample_times, **ALLOCATION_FUNCTION_KWARGS)
+_ALLOCATION_FUNCTION_KWARGS = dict(fill_value=np.nan, dtype=float)
+
+
+def _to_time_type(duration: Real) -> TimeType:
+ if isinstance(duration, TimeType):
+ return duration
+ else:
+ return time_from_float(float(duration), absolute_error=PULSE_TO_WAVEFORM_ERROR)
+
+
+@dataclasses.dataclass(frozen=False, eq=False, repr=False)
+class WaveformMetadata:
+ """Metadata for a waveform. Does not participate in equality and hashing!"""
+
+ minimal_sample_rate: Optional[TimeType] = None
+
+ def __init__(self, **kwargs):
+ for key, value in kwargs.items():
+ setattr(self, key, value)
+
+ def as_dict(self):
+ data = vars(self).copy()
+ for field in dataclasses.fields(self):
+ if field.default is not dataclasses.MISSING:
+ if data[field.name] == field.default:
+ del data[field.name]
+ return data
+
+
+ def __repr__(self):
+ args = ",".join(f"{name}={value!r}"
+ for name, value in self.as_dict().items())
+ return f'{self.__class__.__name__}({args})'
+
+
+class Waveform(metaclass=ABCMeta):
+ """Represents an instantiated PulseTemplate which can be sampled to retrieve arrays of voltage
+ values for the hardware."""
+
+ __sampled_cache = WeakValueDictionary()
+
+ __slots__ = (
+ '_duration', # included in __hash__ and __eq__
+ '_metadata', # excluded from __hash__ and __eq__
+ )
+
+ def __init__(self, duration: TimeType):
+ self._duration = duration
+
+ @property
+ def duration(self) -> TimeType:
+ """The duration of the waveform in time units."""
+ return self._duration
+
+ @property
+ def metadata(self):
+ try:
+ return self._metadata
+ except AttributeError:
+ metadata = self._metadata = WaveformMetadata()
+ return metadata
+
+ @abstractmethod
+ def unsafe_sample(self,
+ channel: ChannelID,
+ sample_times: np.ndarray,
+ output_array: Union[np.ndarray, None] = None) -> np.ndarray:
+ """Sample the waveform at given sample times.
+
+ The unsafe means that there are no sanity checks performed. The provided sample times are assumed to be
+ monotonously increasing and lie in the range of [0, waveform.duration]
+
+ Args:
+ sample_times: Times at which this Waveform will be sampled.
+ output_array: Has to be either None or an array of the same size and type as sample_times. If
+ not None, the sampled values will be written here and this array will be returned
+ Result:
+ The sampled values of this Waveform at the provided sample times. Has the same number of
+ elements as sample_times.
+ """
+
+ def get_sampled(self,
+ channel: ChannelID,
+ sample_times: np.ndarray,
+ output_array: Union[np.ndarray, None] = None) -> np.ndarray:
+ """A wrapper to the unsafe_sample method which caches the result. This method enforces the constrains
+ unsafe_sample expects and caches the result to save memory.
+
+ Args:
+ sample_times: Times at which this Waveform will be sampled.
+ output_array: Has to be either None or an array of the same size and type as sample_times. If an array is
+ given, the sampled values will be written into the given array and it will be returned. Otherwise, a new
+ array will be created and cached to save memory.
+
+ Result:
+ The sampled values of this Waveform at the provided sample times. Is `output_array` if provided
+ """
+ if len(sample_times) == 0:
+ if output_array is None:
+ return np.zeros_like(sample_times, dtype=float)
+ elif len(output_array) == len(sample_times):
+ return output_array
+ else:
+ raise ValueError('Output array length and sample time length are different')
+
+ if not is_monotonic(sample_times):
+ raise ValueError('The sample times are not monotonously increasing')
+ if sample_times[0] < 0 or sample_times[-1] > float(self.duration):
+ raise ValueError(f'The sample times [{sample_times[0]}, ..., {sample_times[-1]}] are not in the range'
+ f' [0, duration={float(self.duration)}]')
+ if channel not in self.defined_channels:
+ raise KeyError('Channel not defined in this waveform: {}'.format(channel))
+
+ constant_value = self.constant_value(channel)
+ if constant_value is None:
+ if output_array is None:
+ # cache the result to save memory
+ result = self.unsafe_sample(channel, sample_times)
+ result.flags.writeable = False
+ key = hash(bytes(result))
+ if key not in self.__sampled_cache:
+ self.__sampled_cache[key] = result
+ return self.__sampled_cache[key]
+ else:
+ if len(output_array) != len(sample_times):
+ raise ValueError('Output array length and sample time length are different')
+ # use the user provided memory
+ return self.unsafe_sample(channel=channel,
+ sample_times=sample_times,
+ output_array=output_array)
+ else:
+ if output_array is None:
+ output_array = np.full_like(sample_times, fill_value=constant_value, dtype=float)
+ else:
+ output_array[:] = constant_value
+ return output_array
+
+ def __hash__(self):
+ if self.__class__.__base__ is not Waveform:
+ # we require direct inheritance because self.__slots__ are the slots of the subclass
+ # we manually add self._duration but not self._metadata here
+ raise NotImplementedError("Waveforms __hash__ and __eq__ implementation requires direct inheritance")
+ return hash(tuple(getattr(self, slot) for slot in self.__slots__)) ^ hash(self._duration)
+
+ def __eq__(self, other):
+ if self.__class__.__base__ is not Waveform:
+ # we require direct inheritance because self.__slots__ are the slots of the subclass
+ # we manually add self._duration but not self._metadata here
+ raise NotImplementedError("Waveforms __hash__ and __eq__ implementation requires direct inheritance")
+ slots = self.__slots__
+ if slots is getattr(other, '__slots__', None):
+ # the __slots__ attribute of self and other are identical objects -> other is of the same type
+ return self._duration == other._duration and all(getattr(self, slot) == getattr(other, slot) for slot in slots)
+ else:
+ # The other class might be more lenient
+ return NotImplemented
+
+ @property
+ @abstractmethod
+ def defined_channels(self) -> AbstractSet[ChannelID]:
+ """The channels this waveform should played on. Use
+ :func:`~qupulse.pulses.instructions.get_measurement_windows` to get a waveform for a subset of these."""
+
+ @abstractmethod
+ def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> 'Waveform':
+ """Unsafe version of :func:`~qupulse.pulses.instructions.get_measurement_windows`."""
+
+ def get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> 'Waveform':
+ """Get a waveform that only describes the channels contained in `channels`.
+
+ Args:
+ channels: A channel set the return value should confine to.
+
+ Raises:
+ KeyError: If `channels` is not a subset of the waveform's defined channels.
+
+ Returns:
+ A waveform with waveform.defined_channels == `channels`
+ """
+ if not channels <= self.defined_channels:
+ raise KeyError('Channels not defined on waveform: {}'.format(channels))
+ if channels == self.defined_channels:
+ return self
+ return self.unsafe_get_subset_for_channels(channels=channels)
+
+ def is_constant(self) -> bool:
+ """Convenience function to check if all channels are constant. The result is equal to
+ `all(waveform.constant_value(ch) is not None for ch in waveform.defined_channels)` but might be more performant.
+
+ Returns:
+ True if all channels have constant values.
+ """
+ return self.constant_value_dict() is not None
+
+ def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
+ result = {ch: self.constant_value(ch) for ch in self.defined_channels}
+ if None in result.values():
+ return None
+ else:
+ return result
+
+ def constant_value(self, channel: ChannelID) -> Optional[float]:
+ """Checks if the requested channel has a constant value and returns it if so.
+
+ Guarantee that this assertion passes for every t in waveform duration:
+ >>> assert waveform.constant_value(channel) is None or waveform.constant_value(t) = waveform.get_sampled(channel, t)
+
+ Args:
+ channel: The channel to check
+
+ Returns:
+ None if there is no guarantee that the channel is constant. The value otherwise.
+ """
+ return None
+
+ def __neg__(self):
+ return FunctorWaveform.from_functor(self, {ch: np.negative for ch in self.defined_channels})
+
+ def __pos__(self):
+ return self
+
+ def _sort_key_for_channels(self) -> Sequence[Tuple[str, int]]:
+ """Makes reproducible sorting by defined channels possible"""
+ return sorted((ch, 0) if isinstance(ch, str) else ('', ch) for ch in self.defined_channels)
+
+ def reversed(self) -> 'Waveform':
+ """Returns a reversed version of this waveform."""
+ # We don't check for constness here because const waveforms are supposed to override this method
+ return ReversedWaveform(self)
+
+
+class TableWaveformEntry(NamedTuple('TableWaveformEntry', [('t', Real),
+ ('v', float),
+ ('interp', InterpolationStrategy)])):
+ def __init__(self, t: float, v: float, interp: InterpolationStrategy):
+ if not callable(interp):
+ raise TypeError('{} is neither callable nor of type InterpolationStrategy'.format(interp))
+
+ def __repr__(self):
+ return f'{type(self).__name__}(t={self.t!r}, v={self.v!r}, interp={self.interp!r})'
+
+
+class TableWaveform(Waveform):
+ EntryInInit = Union[TableWaveformEntry, Tuple[float, float, InterpolationStrategy]]
+
+ """Waveform obtained from instantiating a TablePulseTemplate."""
+
+ __slots__ = ('_table', '_channel_id')
+
+ def __init__(self,
+ channel: ChannelID,
+ waveform_table: Tuple[TableWaveformEntry, ...]) -> None:
+ """Create a new TableWaveform instance.
+
+ Args:
+ waveform_table: A tuple of instantiated and validated table entries
+ """
+ if not isinstance(waveform_table, tuple):
+ warnings.warn("Please use a tuple of TableWaveformEntry to construct TableWaveform directly",
+ category=DeprecationWarning)
+ waveform_table = self._validate_input(waveform_table)
+
+ super().__init__(duration=_to_time_type(waveform_table[-1].t))
+
+ self._table = waveform_table
+ self._channel_id = channel
+
+ @staticmethod
+ def _validate_input(input_waveform_table: Sequence[EntryInInit]) -> Union[Tuple[Real, Real],
+ List[TableWaveformEntry]]:
+ """ Checks that:
+ - the time is increasing,
+ - there are at least two entries
+
+ Optimizations:
+ - removes subsequent entries with same time or voltage values.
+ - checks if the complete waveform is constant. Returns a (duration, value) tuple if this is the case
+
+ Raises:
+ ValueError:
+ - there are less than two entries
+ - the entries are not ordered in time
+ - Any time is negative
+ - The total length is zero
+
+ Returns:
+ A list of de-duplicated table entries
+ OR
+ A (duration, value) tuple if the waveform is constant
+ """
+ # we use an iterator here to avoid duplicate work and be maximally efficient for short tables
+ # We never use StopIteration to abort iteration. It always signifies an error.
+ input_iter = iter(input_waveform_table)
+ try:
+ first_t, first_v, first_interp = next(input_iter)
+ except StopIteration:
+ raise ValueError("Waveform table mut not be empty")
+
+ if first_t != 0.0:
+ raise ValueError('First time entry is not zero.')
+
+ previous_t = 0.0
+ previous_v = first_v
+ output_waveform_table = [TableWaveformEntry(0.0, first_v, first_interp)]
+
+ try:
+ t, v, interp = next(input_iter)
+ except StopIteration:
+ raise ValueError("Waveform table has less than two entries.")
+ if t < 0:
+ raise ValueError('Negative time values are not allowed.')
+
+ # constant_v is None <=> the waveform is constant until up to the current entry
+ constant_v = interp.constant_value((previous_t, previous_v), (t, v))
+
+ for next_t, next_v, next_interp in input_iter:
+ if next_t < t:
+ if next_t < 0:
+ raise ValueError('Negative time values are not allowed.')
+ else:
+ raise ValueError('Times are not increasing.')
+
+ if constant_v is not None and interp.constant_value((t, v), (next_t, next_v)) != constant_v:
+ constant_v = None
+
+ if (previous_t != t or t != next_t) and (previous_v != v or v != next_v):
+ # the time and the value differ both either from the next or the previous
+ # otherwise we skip the entry
+ previous_t = t
+ previous_v = v
+ output_waveform_table.append(TableWaveformEntry(t, v, interp))
+
+ t, v, interp = next_t, next_v, next_interp
+
+ # Until now, we only checked that the time does not decrease. We require an increase because duration == 0
+ # waveforms are ill-formed. t is now the time of the last entry.
+ if t == 0:
+ raise ValueError('Last time entry is zero.')
+
+ if constant_v is not None:
+ # the waveform is constant
+ return t, constant_v
+ else:
+ # we must still add the last entry to the table
+ output_waveform_table.append(TableWaveformEntry(t, v, interp))
+ return output_waveform_table
+
+ def is_constant(self) -> bool:
+ # only correct if `from_table` is used
+ return False
+
+ def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
+ # only correct if `from_table` is used
+ return None
+
+ @classmethod
+ def from_table(cls, channel: ChannelID, table: Sequence[EntryInInit]) -> Union['TableWaveform', 'ConstantWaveform']:
+ table = cls._validate_input(table)
+ if isinstance(table, tuple):
+ duration, amplitude = table
+ return ConstantWaveform(duration=duration, amplitude=amplitude, channel=channel)
+ else:
+ return TableWaveform(channel, tuple(table))
+
+ @property
+ def compare_key(self) -> Any:
+ warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12",
+ DeprecationWarning, stacklevel=2)
+ return self._channel_id, self._table
+
+ def unsafe_sample(self,
+ channel: ChannelID,
+ sample_times: np.ndarray,
+ output_array: Union[np.ndarray, None] = None) -> np.ndarray:
+ if output_array is None:
+ output_array = _ALLOCATION_FUNCTION(sample_times, **_ALLOCATION_FUNCTION_KWARGS)
+
+ if PULSE_TO_WAVEFORM_ERROR:
+ # we need to replace the last entry's t with self.duration
+ *entries, last = self._table
+ entries.append(TableWaveformEntry(float(self.duration), last.v, last.interp))
+ else:
+ entries = self._table
+
+ for entry1, entry2 in pairwise(entries):
+ indices = slice(sample_times.searchsorted(entry1.t, 'left'),
+ sample_times.searchsorted(entry2.t, 'right'))
+ output_array[indices] = \
+ entry2.interp((float(entry1.t), entry1.v),
+ (float(entry2.t), entry2.v),
+ sample_times[indices])
+ return output_array
+
+ @property
+ def defined_channels(self) -> AbstractSet[ChannelID]:
+ return {self._channel_id}
+
+ def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> 'Waveform':
+ return self
+
+ def __repr__(self):
+ return f'{type(self).__name__}(channel={self._channel_id!r}, waveform_table={self._table!r})'
+
+
+class ConstantWaveform(Waveform):
+
+ # TODO: remove
+ _is_constant_waveform = True
+
+ __slots__ = ('_amplitude', '_channel')
+
+ def __init__(self, duration: Real, amplitude: Any, channel: ChannelID):
+ """ Create a qupulse waveform corresponding to a ConstantPulseTemplate """
+ super().__init__(duration=_to_time_type(duration))
+ if hasattr(amplitude, 'shape'):
+ amplitude = amplitude[()]
+ hash(amplitude)
+ self._amplitude = amplitude
+ self._channel = channel
+
+ @classmethod
+ def from_mapping(cls, duration: Real, constant_values: Mapping[ChannelID, float]) -> Union['ConstantWaveform',
+ 'MultiChannelWaveform']:
+ """Construct a ConstantWaveform or a MultiChannelWaveform of ConstantWaveforms with given duration and values"""
+ assert constant_values
+ duration = _to_time_type(duration)
+ if len(constant_values) == 1:
+ (channel, amplitude), = constant_values.items()
+ return cls(duration, amplitude=amplitude, channel=channel)
+ else:
+ return MultiChannelWaveform([cls(duration, amplitude=amplitude, channel=channel)
+ for channel, amplitude in constant_values.items()])
+
+ def is_constant(self) -> bool:
+ return True
+
+ def constant_value(self, channel: ChannelID) -> Optional[float]:
+ assert channel == self._channel
+ return self._amplitude
+
+ def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
+ return {self._channel: self._amplitude}
+
+ @property
+ def defined_channels(self) -> AbstractSet[ChannelID]:
+ """The channels this waveform should played on. Use
+ :func:`~qupulse.pulses.instructions.get_measurement_windows` to get a waveform for a subset of these."""
+
+ return {self._channel}
+
+ @property
+ def compare_key(self) -> Tuple[Any, ...]:
+ warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12",
+ DeprecationWarning, stacklevel=2)
+ return self._duration, self._amplitude, self._channel
+
+ def unsafe_sample(self,
+ channel: ChannelID,
+ sample_times: np.ndarray,
+ output_array: Union[np.ndarray, None] = None) -> np.ndarray:
+ if output_array is None:
+ return np.full_like(sample_times, fill_value=self._amplitude, dtype=float)
+ else:
+ output_array[:] = self._amplitude
+ return output_array
+
+ def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> Waveform:
+ """Unsafe version of :func:`~qupulse.pulses.instructions.get_measurement_windows`."""
+ return self
+
+ def __repr__(self):
+ return f"{type(self).__name__}(duration={self.duration!r}, "\
+ f"amplitude={self._amplitude!r}, channel={self._channel!r})"
+
+ def reversed(self) -> 'Waveform':
+ return self
+
+
+class FunctionWaveform(Waveform):
+ """Waveform obtained from instantiating a FunctionPulseTemplate."""
+
+ __slots__ = ('_expression', '_channel_id')
+
+ def __init__(self, expression: ExpressionScalar,
+ duration: float,
+ channel: ChannelID) -> None:
+ """Creates a new FunctionWaveform instance.
+
+ Args:
+ expression: The function represented by this FunctionWaveform
+ as a mathematical expression where 't' denotes the time variable. It must not have other variables
+ duration: The duration of the waveform
+ measurement_windows: A list of measurement windows
+ channel: The channel this waveform is played on
+ """
+
+ if set(expression.variables) - set('t'):
+ raise ValueError('FunctionWaveforms may not depend on anything but "t"')
+ elif not expression.variables:
+ warnings.warn("Constant FunctionWaveform is not recommended as the constant propagation will be suboptimal",
+ category=ConstantFunctionPulseTemplateWarning)
+ super().__init__(duration=_to_time_type(duration))
+ self._expression = expression
+ self._channel_id = channel
+
+ @classmethod
+ def from_expression(cls, expression: ExpressionScalar, duration: float, channel: ChannelID) -> Union['FunctionWaveform', ConstantWaveform]:
+ if expression.variables:
+ return cls(expression, duration, channel)
+ else:
+ return ConstantWaveform(amplitude=expression.evaluate_numeric(), duration=duration, channel=channel)
+
+ def is_constant(self) -> bool:
+ # only correct if `from_expression` is used
+ return False
+
+ def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
+ # only correct if `from_expression` is used
+ return None
+
+ @property
+ def defined_channels(self) -> AbstractSet[ChannelID]:
+ return {self._channel_id}
+
+ @property
+ def compare_key(self) -> Any:
+ warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12",
+ DeprecationWarning, stacklevel=2)
+ return self._channel_id, self._expression, self._duration
+
+ @property
+ def duration(self) -> TimeType:
+ return self._duration
+
+ def unsafe_sample(self,
+ channel: ChannelID,
+ sample_times: np.ndarray,
+ output_array: Union[np.ndarray, None] = None) -> np.ndarray:
+ evaluated = self._expression.evaluate_numeric(t=sample_times)
+ if output_array is None:
+ if self._expression.variables:
+ return evaluated.astype(float)
+ else:
+ return np.full_like(sample_times, fill_value=float(evaluated), dtype=float)
+ else:
+ output_array[:] = evaluated
+ return output_array
+
+ def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> Waveform:
+ return self
+
+ def __repr__(self):
+ return f"{type(self).__name__}(duration={self.duration!r}, "\
+ f"expression={self._expression!r}, channel={self._channel_id!r})"
+
+
+class SequenceWaveform(Waveform):
+ """This class allows putting multiple PulseTemplate together in one waveform on the hardware."""
+
+ __slots__ = ('_sequenced_waveforms', )
+
+ def __init__(self, sub_waveforms: Iterable[Waveform]):
+ """Use Waveform.from_sequence for optimal construction
+
+ :param subwaveforms: All waveforms must have the same defined channels
+ """
+ if not sub_waveforms:
+ raise ValueError(
+ "SequenceWaveform cannot be constructed without channel waveforms."
+ )
+
+ # do not fail on iterators although we do not allow them as an argument
+ sequenced_waveforms = tuple(sub_waveforms)
+
+ super().__init__(duration=sum(waveform.duration for waveform in sequenced_waveforms))
+ self._sequenced_waveforms = sequenced_waveforms
+
+ defined_channels = self._sequenced_waveforms[0].defined_channels
+ if not all(waveform.defined_channels == defined_channels
+ for waveform in itertools.islice(self._sequenced_waveforms, 1, None)):
+ for waveform in self._sequenced_waveforms[1:]:
+ if not waveform.defined_channels == self.defined_channels:
+ print(f"SequenceWaveform: defined channels {self.defined_channels} do not match {waveform.defined_channels} ")
+ raise ValueError(
+ "SequenceWaveform cannot be constructed from waveforms of different"
+ "defined channels."
+ )
+
+ @classmethod
+ def from_sequence(cls, waveforms: Sequence['Waveform']) -> 'Waveform':
+ """Returns a waveform the represents the given sequence of waveforms. Applies some optimizations."""
+ assert waveforms, "Sequence must not be empty"
+ if len(waveforms) == 1:
+ return waveforms[0]
+
+ flattened = []
+ constant_values = waveforms[0].constant_value_dict()
+ for wf in waveforms:
+ if constant_values and constant_values != wf.constant_value_dict():
+ constant_values = None
+ if isinstance(wf, cls):
+ flattened.extend(wf.sequenced_waveforms)
+ else:
+ flattened.append(wf)
+ if constant_values is None:
+ return cls(sub_waveforms=flattened)
+ else:
+ duration = sum(wf.duration for wf in flattened)
+ return ConstantWaveform.from_mapping(duration, constant_values)
+
+ def is_constant(self) -> bool:
+ # only correct if from_sequence is used for construction
+ return False
+
+ def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
+ # only correct if from_sequence is used for construction
+ return None
+
+ def constant_value(self, channel: ChannelID) -> Optional[float]:
+ v = None
+ for wf in self._sequenced_waveforms:
+ wf_cv = wf.constant_value(channel)
+ if wf_cv is None:
+ return None
+ elif wf_cv == v:
+ continue
+ elif v is None:
+ v = wf_cv
+ else:
+ assert v != wf_cv
+ return None
+ return v
+
+ @property
+ def defined_channels(self) -> AbstractSet[ChannelID]:
+ return self._sequenced_waveforms[0].defined_channels
+
+ def unsafe_sample(self,
+ channel: ChannelID,
+ sample_times: np.ndarray,
+ output_array: Union[np.ndarray, None] = None) -> np.ndarray:
+ if output_array is None:
+ output_array = _ALLOCATION_FUNCTION(sample_times, **_ALLOCATION_FUNCTION_KWARGS)
+ time = 0
+ for subwaveform in self._sequenced_waveforms:
+ # before you change anything here, make sure to understand the difference between basic and advanced
+ # indexing in numpy and their copy/reference behaviour
+ end = time + subwaveform.duration
+
+ indices = slice(*sample_times.searchsorted((float(time), float(end)), 'left'))
+ subwaveform.unsafe_sample(channel=channel,
+ sample_times=sample_times[indices]-np.float64(time),
+ output_array=output_array[indices])
+ time = end
+ return output_array
+
+ @property
+ def compare_key(self) -> Tuple[Waveform]:
+ warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12",
+ DeprecationWarning, stacklevel=2)
+ return self._sequenced_waveforms
+
+ @property
+ def duration(self) -> TimeType:
+ return self._duration
+
+ def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> 'Waveform':
+ return SequenceWaveform.from_sequence([
+ sub_waveform.unsafe_get_subset_for_channels(channels & sub_waveform.defined_channels)
+ for sub_waveform in self._sequenced_waveforms if sub_waveform.defined_channels & channels])
+
+ @property
+ def sequenced_waveforms(self) -> Sequence[Waveform]:
+ return self._sequenced_waveforms
+
+ def __repr__(self):
+ return f"{type(self).__name__}({self._sequenced_waveforms})"
+
+
+class MultiChannelWaveform(Waveform):
+ """A MultiChannelWaveform is a Waveform object that allows combining arbitrary Waveform objects
+ to into a single waveform defined for several channels.
+
+ The number of channels used by the MultiChannelWaveform object is the sum of the channels used
+ by the Waveform objects it consists of.
+
+ MultiChannelWaveform allows an arbitrary mapping of channels defined by the Waveforms it
+ consists of and the channels it defines. For example, if the MultiChannelWaveform consists
+ of a two Waveform objects A and B which define two channels each, then the channels of the
+ MultiChannelWaveform may be 0: A.1, 1: B.0, 2: B.1, 3: A.0 where A.0 means channel 0 of Waveform
+ object A.
+
+ The following constraints must hold:
+ - The durations of all Waveform objects must be equal.
+ - The channel mapping must be sane, i.e., no channel of the MultiChannelWaveform must be
+ assigned more than one channel of any Waveform object it consists of
+ """
+
+ __slots__ = ('_sub_waveforms', '_defined_channels')
+
+ def __init__(self, sub_waveforms: List[Waveform]) -> None:
+ """Create a new MultiChannelWaveform instance.
+ Use `MultiChannelWaveform.from_parallel` for optimal construction.
+
+ Requires a list of subwaveforms in the form (Waveform, List(int)) where the list defines
+ the channel mapping, i.e., a value y at index x in the list means that channel x of the
+ subwaveform will be mapped to channel y of this MultiChannelWaveform object.
+
+ Args:
+ sub_waveforms: The list of sub waveforms of this
+ MultiChannelWaveform. List might get sorted!
+ Raises:
+ ValueError, if a channel mapping is out of bounds of the channels defined by this
+ MultiChannelWaveform
+ ValueError, if several subwaveform channels are assigned to a single channel of this
+ MultiChannelWaveform
+ ValueError, if subwaveforms have inconsistent durations
+ """
+
+ if not sub_waveforms:
+ raise ValueError(
+ "MultiChannelWaveform cannot be constructed without channel waveforms."
+ )
+
+ # sort the waveforms with their defined channels to make compare key reproducible
+ if not isinstance(sub_waveforms, list):
+ sub_waveforms = list(sub_waveforms)
+ sub_waveforms.sort(key=lambda wf: wf._sort_key_for_channels())
+
+ super().__init__(duration=sub_waveforms[0].duration)
+ self._sub_waveforms = tuple(sub_waveforms)
+
+ defined_channels = set()
+ for waveform in self._sub_waveforms:
+ if waveform.defined_channels & defined_channels:
+ raise ValueError('Channel may not be defined in multiple waveforms',
+ waveform.defined_channels & defined_channels)
+ defined_channels |= waveform.defined_channels
+ self._defined_channels = frozenset(defined_channels)
+
+ if not all(isclose(waveform.duration, self.duration) for waveform in self._sub_waveforms[1:]):
+ # meaningful error message:
+ durations = {}
+
+ for waveform in self._sub_waveforms:
+ for duration, channels in durations.items():
+ if isclose(waveform.duration, duration):
+ channels.update(waveform.defined_channels)
+ break
+ else:
+ durations[waveform.duration] = set(waveform.defined_channels)
+
+ raise ValueError(
+ "MultiChannelWaveform cannot be constructed from channel waveforms of different durations.",
+ durations
+ )
+
+ @staticmethod
+ def from_parallel(waveforms: Sequence[Waveform]) -> Waveform:
+ assert waveforms, "ARgument must not be empty"
+ if len(waveforms) == 1:
+ return waveforms[0]
+
+ # we do not look at constant values here because there is no benefit. We would need to construct a new
+ # MultiChannelWaveform anyways
+
+ # avoid unnecessary multi channel nesting
+ flattened = []
+ for waveform in waveforms:
+ if isinstance(waveform, MultiChannelWaveform):
+ flattened.extend(waveform._sub_waveforms)
+ else:
+ flattened.append(waveform)
+
+ return MultiChannelWaveform(flattened)
+
+ def is_constant(self) -> bool:
+ return all(wf.is_constant() for wf in self._sub_waveforms)
+
+ def constant_value(self, channel: ChannelID) -> Optional[float]:
+ return self[channel].constant_value(channel)
+
+ def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
+ d = {}
+ for wf in self._sub_waveforms:
+ wf_d = wf.constant_value_dict()
+ if wf_d is None:
+ return None
+ else:
+ d.update(wf_d)
+ return d
+
+ @property
+ def duration(self) -> TimeType:
+ return self._sub_waveforms[0].duration
+
+ def __getitem__(self, key: ChannelID) -> Waveform:
+ for waveform in self._sub_waveforms:
+ if key in waveform.defined_channels:
+ return waveform
+ raise KeyError('Unknown channel ID: {}'.format(key), key)
+
+ @property
+ def defined_channels(self) -> AbstractSet[ChannelID]:
+ return self._defined_channels
+
+ @property
+ def compare_key(self) -> Any:
+ warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12",
+ DeprecationWarning, stacklevel=2)
+ return self._sub_waveforms
+
+ def unsafe_sample(self,
+ channel: ChannelID,
+ sample_times: np.ndarray,
+ output_array: Union[np.ndarray, None] = None) -> np.ndarray:
+ return self[channel].unsafe_sample(channel, sample_times, output_array)
+
+ def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> 'Waveform':
+ relevant_sub_waveforms = [swf for swf in self._sub_waveforms if swf.defined_channels & channels]
+ if len(relevant_sub_waveforms) == 1:
+ return relevant_sub_waveforms[0].get_subset_for_channels(channels)
+ elif len(relevant_sub_waveforms) > 1:
+ return MultiChannelWaveform.from_parallel(
+ [sub_waveform.get_subset_for_channels(channels & sub_waveform.defined_channels)
+ for sub_waveform in relevant_sub_waveforms])
+ else:
+ raise KeyError('Unknown channels: {}'.format(channels))
+
+ def __repr__(self):
+ return f"{type(self).__name__}({self._sub_waveforms!r})"
+
+
+class RepetitionWaveform(Waveform):
+ """This class allows putting multiple PulseTemplate together in one waveform on the hardware."""
+
+ __slots__ = ('_body', '_repetition_count')
+
+ def __init__(self, body: Waveform, repetition_count: int):
+ repetition_count = checked_int_cast(repetition_count)
+ if repetition_count < 1 or not isinstance(repetition_count, int):
+ raise ValueError('Repetition count must be an integer >0')
+
+ super().__init__(duration=body.duration * repetition_count)
+ self._body = body
+ self._repetition_count = repetition_count
+
+ @classmethod
+ def from_repetition_count(cls, body: Waveform, repetition_count: int) -> Waveform:
+ constant_values = body.constant_value_dict()
+ if constant_values is None:
+ return RepetitionWaveform(body, repetition_count)
+ else:
+ return ConstantWaveform.from_mapping(body.duration * repetition_count, constant_values)
+
+ @property
+ def defined_channels(self) -> AbstractSet[ChannelID]:
+ return self._body.defined_channels
+
+ def unsafe_sample(self,
+ channel: ChannelID,
+ sample_times: np.ndarray,
+ output_array: Union[np.ndarray, None] = None) -> np.ndarray:
+ if output_array is None:
+ output_array = _ALLOCATION_FUNCTION(sample_times, **_ALLOCATION_FUNCTION_KWARGS)
+ body_duration = self._body.duration
+ time = 0
+ for _ in range(self._repetition_count):
+ end = time + body_duration
+ indices = slice(*sample_times.searchsorted((float(time), float(end)), 'left'))
+ self._body.unsafe_sample(channel=channel,
+ sample_times=sample_times[indices] - float(time),
+ output_array=output_array[indices])
+ time = end
+ return output_array
+
+ @property
+ def compare_key(self) -> Tuple[Any, int]:
+ warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12",
+ DeprecationWarning, stacklevel=2)
+ return self._body.compare_key, self._repetition_count
+
+ def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> Waveform:
+ return RepetitionWaveform.from_repetition_count(
+ body=self._body.unsafe_get_subset_for_channels(channels),
+ repetition_count=self._repetition_count)
+
+ def is_constant(self) -> bool:
+ return self._body.is_constant()
+
+ def constant_value(self, channel: ChannelID) -> Optional[float]:
+ return self._body.constant_value(channel)
+
+ def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
+ return self._body.constant_value_dict()
+
+ def __repr__(self):
+ return f"{type(self).__name__}(body={self._body!r}, repetition_count={self._repetition_count!r})"
+
+
+class TransformingWaveform(Waveform):
+ __slots__ = ('_inner_waveform', '_transformation', '_cached_data', '_cached_times')
+
+ def __init__(self, inner_waveform: Waveform, transformation: Transformation):
+ """"""
+ super(TransformingWaveform, self).__init__(duration=inner_waveform.duration)
+ self._inner_waveform = inner_waveform
+ self._transformation = transformation
+
+ # cache data of inner channels based identified and invalidated by the sample times
+ self._cached_data = None
+ self._cached_times = lambda: None
+
+ def __hash__(self):
+ return hash((self._inner_waveform, self._transformation))
+
+ def __eq__(self, other):
+ if getattr(other, '__slots__', None) is self.__slots__:
+ return self._inner_waveform == other._inner_waveform and self._transformation == other._transformation
+ return NotImplemented
+
+ @classmethod
+ def from_transformation(cls, inner_waveform: Waveform, transformation: Transformation) -> Waveform:
+ constant_values = inner_waveform.constant_value_dict()
+
+ if constant_values is None or not transformation.is_constant_invariant():
+ return cls(inner_waveform, transformation)
+
+ transformed_constant_values = {key: value for key, value in transformation(0., constant_values).items()}
+ return ConstantWaveform.from_mapping(inner_waveform.duration, transformed_constant_values)
+
+ def is_constant(self) -> bool:
+ # only true if `from_transformation` was used
+ return False
+
+ def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
+ # only true if `from_transformation` was used
+ return None
+
+ def constant_value(self, channel: ChannelID) -> Optional[float]:
+ if not self._transformation.is_constant_invariant():
+ return None
+ in_channels = self._transformation.get_input_channels({channel})
+ in_values = {ch: self._inner_waveform.constant_value(ch) for ch in in_channels}
+ if any(val is None for val in in_values.values()):
+ return None
+ else:
+ return self._transformation(0., in_values)[channel]
+
+ @property
+ def inner_waveform(self) -> Waveform:
+ return self._inner_waveform
+
+ @property
+ def transformation(self) -> Transformation:
+ return self._transformation
+
+ @property
+ def defined_channels(self) -> AbstractSet[ChannelID]:
+ return self.transformation.get_output_channels(self.inner_waveform.defined_channels)
+
+ @property
+ def compare_key(self) -> Tuple[Waveform, Transformation]:
+ warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12",
+ DeprecationWarning, stacklevel=2)
+ return self.inner_waveform, self.transformation
+
+ def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> 'SubsetWaveform':
+ return SubsetWaveform(self, channel_subset=channels)
+
+ def unsafe_sample(self,
+ channel: ChannelID,
+ sample_times: np.ndarray,
+ output_array: Union[np.ndarray, None] = None) -> np.ndarray:
+ if self._cached_times() is not sample_times:
+ self._cached_data = dict()
+ self._cached_times = ref(sample_times)
+
+ if channel not in self._cached_data:
+
+ inner_channels = self.transformation.get_input_channels({channel})
+
+ inner_data = {inner_channel: self.inner_waveform.unsafe_sample(inner_channel, sample_times)
+ for inner_channel in inner_channels}
+
+ outer_data = self.transformation(sample_times, inner_data)
+ self._cached_data.update(outer_data)
+
+ if output_array is None:
+ output_array = self._cached_data[channel]
+ else:
+ output_array[:] = self._cached_data[channel]
+
+ return output_array
+
+
+class SubsetWaveform(Waveform):
+ __slots__ = ('_inner_waveform', '_channel_subset')
+
+ def __init__(self, inner_waveform: Waveform, channel_subset: Set[ChannelID]):
+ super().__init__(duration=inner_waveform.duration)
+ self._inner_waveform = inner_waveform
+ self._channel_subset = frozenset(channel_subset)
+
+ @property
+ def inner_waveform(self) -> Waveform:
+ return self._inner_waveform
+
+ @property
+ def defined_channels(self) -> FrozenSet[ChannelID]:
+ return self._channel_subset
+
+ @property
+ def compare_key(self) -> Tuple[frozenset, Waveform]:
+ warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12",
+ DeprecationWarning, stacklevel=2)
+ return self.defined_channels, self.inner_waveform
+
+ def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> Waveform:
+ return self.inner_waveform.get_subset_for_channels(channels)
+
+ def unsafe_sample(self,
+ channel: ChannelID,
+ sample_times: np.ndarray,
+ output_array: Union[np.ndarray, None] = None) -> np.ndarray:
+ return self.inner_waveform.unsafe_sample(channel, sample_times, output_array)
+
+ def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
+ d = self._inner_waveform.constant_value_dict()
+ if d is not None:
+ return {ch: d[ch] for ch in self._channel_subset}
+
+ def constant_value(self, channel: ChannelID) -> Optional[float]:
+ if channel not in self._channel_subset:
+ raise KeyError(channel)
+ return self._inner_waveform.constant_value(channel)
+
+
+class ArithmeticWaveform(Waveform):
+ """Channels only present in one waveform have the operations neutral element on the other."""
+
+ numpy_operator_map = {'+': np.add,
+ '-': np.subtract}
+ operator_map = {'+': operator.add,
+ '-': operator.sub}
+
+ rhs_only_map = {'+': operator.pos,
+ '-': operator.neg}
+ numpy_rhs_only_map = {'+': np.positive,
+ '-': np.negative}
+
+ __slots__ = ('_lhs', '_rhs', '_arithmetic_operator')
+
+ def __init__(self,
+ lhs: Waveform,
+ arithmetic_operator: str,
+ rhs: Waveform):
+ super().__init__(duration=lhs.duration)
+ self._lhs = lhs
+ self._rhs = rhs
+ self._arithmetic_operator = arithmetic_operator
+
+ assert np.isclose(float(self._lhs.duration), float(self._rhs.duration))
+ assert arithmetic_operator in self.operator_map
+
+ @classmethod
+ def from_operator(cls, lhs: Waveform, arithmetic_operator: str, rhs: Waveform):
+ # one could optimize rhs_cv to being only created if lhs_cv is not None but this makes the code harder to read
+ lhs_cv = lhs.constant_value_dict()
+ rhs_cv = rhs.constant_value_dict()
+ if lhs_cv is None or rhs_cv is None:
+ return cls(lhs, arithmetic_operator, rhs)
+
+ else:
+ constant_values = dict(lhs_cv)
+ op = cls.operator_map[arithmetic_operator]
+ rhs_op = cls.rhs_only_map[arithmetic_operator]
+
+ for ch, rhs_val in rhs_cv.items():
+ if ch in constant_values:
+ constant_values[ch] = op(constant_values[ch], rhs_val)
+ else:
+ constant_values[ch] = rhs_op(rhs_val)
+
+ duration = lhs.duration
+ assert isclose(duration, rhs.duration)
+
+ return ConstantWaveform.from_mapping(duration, constant_values)
+
+ def constant_value(self, channel: ChannelID) -> Optional[float]:
+ if channel not in self._rhs.defined_channels:
+ return self._lhs.constant_value(channel)
+ rhs = self._rhs.constant_value(channel)
+ if rhs is None:
+ return None
+
+ if channel in self._lhs.defined_channels:
+ lhs = self._lhs.constant_value(channel)
+ if lhs is None:
+ return None
+
+ return self.operator_map[self._arithmetic_operator](lhs, rhs)
+ else:
+ return self.rhs_only_map[self._arithmetic_operator](rhs)
+
+ def is_constant(self) -> bool:
+ # only correct if from_operator is used
+ return False
+
+ def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
+ # only correct if from_operator is used
+ return None
+
+ @property
+ def lhs(self) -> Waveform:
+ return self._lhs
+
+ @property
+ def rhs(self) -> Waveform:
+ return self._rhs
+
+ @property
+ def arithmetic_operator(self) -> str:
+ return self._arithmetic_operator
+
+ @property
+ def duration(self) -> TimeType:
+ return self._lhs.duration
+
+ @property
+ def defined_channels(self) -> AbstractSet[ChannelID]:
+ return self._lhs.defined_channels | self._rhs.defined_channels
+
+ def unsafe_sample(self,
+ channel: ChannelID,
+ sample_times: np.ndarray,
+ output_array: Union[np.ndarray, None] = None) -> np.ndarray:
+ if channel in self._lhs.defined_channels:
+ lhs = self._lhs.unsafe_sample(channel=channel, sample_times=sample_times, output_array=output_array)
+ else:
+ lhs = None
+
+ if channel in self._rhs.defined_channels:
+ rhs = self._rhs.unsafe_sample(channel=channel, sample_times=sample_times,
+ output_array=None if lhs is not None else output_array)
+ else:
+ rhs = None
+
+ if rhs is not None and lhs is not None:
+ arithmetic_operator = self.numpy_operator_map[self._arithmetic_operator]
+ if output_array is None:
+ output_array = lhs
+ return arithmetic_operator(lhs, rhs, out=output_array)
+
+ else:
+ if lhs is None:
+ assert rhs is not None, "channel %r not in defined channels (internal bug)" % channel
+ return self.numpy_rhs_only_map[self._arithmetic_operator](rhs, out=output_array)
+ else:
+ return lhs
+
+ def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> Waveform:
+ # TODO: optimization possible
+ return SubsetWaveform(self, channels)
+
+ @property
+ def compare_key(self) -> Tuple[str, Waveform, Waveform]:
+ warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12",
+ DeprecationWarning, stacklevel=2)
+ return self._arithmetic_operator, self._lhs, self._rhs
+
+
+class FunctorWaveform(Waveform):
+ # TODO: Use Protocol to enforce that it accepts second argument has the keyword out
+ Functor = callable
+
+ __slots__ = ('_inner_waveform', '_functor')
+
+ """Apply a channel wise functor that works inplace to all results. The functor must accept two arguments"""
+ def __init__(self, inner_waveform: Waveform, functor: Mapping[ChannelID, Functor]):
+ super(FunctorWaveform, self).__init__(duration=inner_waveform.duration)
+ self._inner_waveform = inner_waveform
+ self._functor = dict(functor.items())
+
+ assert set(functor.keys()) == inner_waveform.defined_channels, ("There is no default identity mapping (yet)."
+ "File an issue on github if you need it.")
+
+ @classmethod
+ def from_functor(cls, inner_waveform: Waveform, functor: Mapping[ChannelID, Functor]):
+ constant_values = inner_waveform.constant_value_dict()
+ if constant_values is None:
+ return FunctorWaveform(inner_waveform, functor)
+
+ funced_constant_values = {ch: functor[ch](val) for ch, val in constant_values.items()}
+ return ConstantWaveform.from_mapping(inner_waveform.duration, funced_constant_values)
+
+ def is_constant(self) -> bool:
+ # only correct if `from_functor` was used
+ return False
+
+ def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]:
+ # only correct if `from_functor` was used
+ return None
+
+ def constant_value(self, channel: ChannelID) -> Optional[float]:
+ inner = self._inner_waveform.constant_value(channel)
+ if inner is None:
+ return None
+ else:
+ return self._functor[channel](inner)
+
+ @property
+ def defined_channels(self) -> AbstractSet[ChannelID]:
+ return self._inner_waveform.defined_channels
+
+ def unsafe_sample(self,
+ channel: ChannelID,
+ sample_times: np.ndarray,
+ output_array: Union[np.ndarray, None] = None) -> np.ndarray:
+ inner_output = self._inner_waveform.unsafe_sample(channel, sample_times, output_array)
+ return self._functor[channel](inner_output, out=inner_output)
+
+ def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> Waveform:
+ return FunctorWaveform.from_functor(
+ self._inner_waveform.unsafe_get_subset_for_channels(channels),
+ {ch: self._functor[ch] for ch in channels})
+
+ @property
+ def compare_key(self) -> Tuple[Waveform, FrozenSet]:
+ warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12",
+ DeprecationWarning, stacklevel=2)
+ return self._inner_waveform, frozenset(self._functor.items())
+
+
+class ReversedWaveform(Waveform):
+ """Reverses the inner waveform in time."""
+
+ __slots__ = ('_inner',)
+
+ def __init__(self, inner: Waveform):
+ super().__init__(duration=inner.duration)
+ self._inner = inner
+
+ @classmethod
+ def from_to_reverse(cls, inner: Waveform) -> Waveform:
+ if inner.constant_value_dict():
+ return inner
+ else:
+ return cls(inner)
+
+ def unsafe_sample(self, channel: ChannelID, sample_times: np.ndarray,
+ output_array: Union[np.ndarray, None] = None) -> np.ndarray:
+ inner_sample_times = (float(self.duration) - sample_times)[::-1]
+ if output_array is None:
+ return self._inner.unsafe_sample(channel, inner_sample_times, None)[::-1]
+ else:
+ inner_output_array = output_array[::-1]
+ inner_output_array = self._inner.unsafe_sample(channel, inner_sample_times, output_array=inner_output_array)
+ if id(inner_output_array.base) not in (id(output_array), id(output_array.base)):
+ # TODO: is there a guarantee by numpy we never end up here?
+ output_array[:] = inner_output_array[::-1]
+ return output_array
+
+ @property
+ def defined_channels(self) -> AbstractSet[ChannelID]:
+ return self._inner.defined_channels
+
+ def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> 'Waveform':
+ return ReversedWaveform.from_to_reverse(self._inner.unsafe_get_subset_for_channels(channels))
+
+ @property
+ def compare_key(self) -> Hashable:
+ warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12",
+ DeprecationWarning, stacklevel=2)
+ return self._inner.compare_key
+
+ def reversed(self) -> 'Waveform':
+ return self._inner
+
+ def __repr__(self):
+ return f"ReversedWaveform(inner={self._inner!r})"
diff --git a/qupulse/pulses/__init__.py b/qupulse/pulses/__init__.py
index 4a8e1016f..32705149f 100644
--- a/qupulse/pulses/__init__.py
+++ b/qupulse/pulses/__init__.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
"""This is the central package for defining pulses. All :class:`~qupulse.pulses.pulse_template.PulseTemplate`
subclasses that are final and ready to be used are imported here with their recommended abbreviation as an alias.
@@ -19,15 +23,6 @@
ArithmeticAtomicPulseTemplate as ArithmeticAtomicPT
from qupulse.pulses.time_reversal_pulse_template import TimeReversalPulseTemplate as TimeReversalPT
-import warnings
-with warnings.catch_warnings():
- warnings.simplefilter('ignore')
- # ensure this is included.. it adds a deserialization handler for pulse_template_parameter_mapping.MappingPT
- # which is not present otherwise
- import qupulse.pulses.pulse_template_parameter_mapping
- del qupulse
-del warnings
-
__all__ = ["FunctionPT", "ForLoopPT", "AtomicMultiChannelPT", "MappingPT", "RepetitionPT", "SequencePT", "TablePT",
"PointPT", "ConstantPT", "AbstractPT", "ParallelConstantChannelPT", "ArithmeticPT", "ArithmeticAtomicPT",
diff --git a/qupulse/pulses/abstract_pulse_template.py b/qupulse/pulses/abstract_pulse_template.py
index 05e75307c..e26500d3f 100644
--- a/qupulse/pulses/abstract_pulse_template.py
+++ b/qupulse/pulses/abstract_pulse_template.py
@@ -1,10 +1,15 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
from typing import Set, Optional, Dict, Any, cast
from functools import partial, partialmethod
import warnings
from qupulse import ChannelID
from qupulse.expressions import ExpressionScalar
-from qupulse.serialization import PulseRegistryType, Serializable
+from qupulse.pulses.metadata import TemplateMetadata
+from qupulse.serialization import PulseRegistryType
from qupulse.pulses.pulse_template import PulseTemplate
@@ -12,6 +17,8 @@
class AbstractPulseTemplate(PulseTemplate):
+
+
_PROPERTY_DOC = """Abstraction of :py:attr:`.PulseTemplate.{name}`. Raises :class:`.NotSpecifiedError` if the
abstract template is unlinked or the property was not specified."""
@@ -23,7 +30,7 @@ def __init__(self, identifier: str,
integral: Optional[Dict[ChannelID, ExpressionScalar]]=None,
duration: Optional[ExpressionScalar]=None,
registry: Optional[PulseRegistryType]=None):
- """This pulse template can be used as a place holder for a pulse template with a defined interface. Pulse
+ """This pulse template can be used as a placeholder for a pulse template with a defined interface. Pulse
template properties like :func:`defined_channels` can be passed on initialization to declare those properties who make
up the interface. Omitted properties raise an :class:`.NotSpecifiedError` exception if accessed. Properties
which have been accessed are marked as "frozen".
@@ -75,12 +82,17 @@ def __init__(self, identifier: str,
self._register(registry=registry)
+ @property
+ def metadata(self) -> TemplateMetadata:
+ raise NotImplementedError('AbstractPulseTemplate does not support metadata yet. '
+ 'Please file an issue if you need this feature.')
+
def link_to(self, target: PulseTemplate, serialize_linked: bool=None):
"""Link to another pulse template.
Args:
target: Forward all getattr calls to this pulse template
- serialize_linked: If true, serialization will be forwarded. Otherwise serialization will ignore the link
+ serialize_linked: If true, serialization will be forwarded. Otherwise, serialization will ignore the link
"""
if self._linked_target:
raise RuntimeError('Cannot is already linked. If you REALLY need to relink call unlink() first.')
@@ -131,10 +143,8 @@ def _forward_if_linked(self, method_name: str, *args, **kwargs) -> Any:
else:
raise RuntimeError('Cannot call "%s". No linked target to refer to', method_name)
- def _internal_create_program(self, **kwargs):
- raise NotImplementedError('this should never be called as we overrode _create_program') # pragma: no cover
-
_create_program = partialmethod(_forward_if_linked, '_create_program')
+ _build_program = partialmethod(_forward_if_linked, '_build_program')
defined_channels = property(partial(_get_property, property_name='defined_channels'),
doc=_PROPERTY_DOC.format(name='defined_channels'))
diff --git a/qupulse/pulses/arithmetic_pulse_template.py b/qupulse/pulses/arithmetic_pulse_template.py
index 7a2735ee2..e2cc1c00a 100644
--- a/qupulse/pulses/arithmetic_pulse_template.py
+++ b/qupulse/pulses/arithmetic_pulse_template.py
@@ -1,3 +1,6 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
from typing import Any, Dict, List, Set, Optional, Union, Mapping, FrozenSet, cast, Callable
from numbers import Real
@@ -7,6 +10,8 @@
import sympy
from qupulse.expressions import ExpressionScalar, ExpressionLike
+from qupulse.program import ProgramBuilder
+from qupulse.pulses.metadata import TemplateMetadata
from qupulse.serialization import Serializer, PulseRegistryType
from qupulse.parameter_scope import Scope
@@ -14,8 +19,8 @@
from qupulse.utils.types import ChannelID
from qupulse.pulses.measurement import MeasurementWindow
from qupulse.pulses.pulse_template import AtomicPulseTemplate, PulseTemplate
-from qupulse._program.waveforms import Waveform, ArithmeticWaveform, TransformingWaveform
-from qupulse._program.transformation import Transformation, ScalingTransformation, OffsetTransformation,\
+from qupulse.program.waveforms import Waveform, ArithmeticWaveform, TransformingWaveform
+from qupulse.program.transformation import Transformation, ScalingTransformation, OffsetTransformation,\
IdentityTransformation
@@ -42,7 +47,9 @@ def __init__(self,
silent_atomic: bool = False,
measurements: List = None,
identifier: str = None,
- registry: PulseRegistryType = None):
+ registry: PulseRegistryType = None,
+ metadata: TemplateMetadata | dict = None,
+ ):
"""Apply an operation (+ or -) channel wise to two atomic pulse templates. Channels only present in one pulse
template have the operations neutral element on the other. The operations are defined in
`ArithmeticWaveform.operator_map`.
@@ -57,7 +64,7 @@ def __init__(self,
identifier: See AtomicPulseTemplate
registry: See qupulse.serialization.PulseRegistry
"""
- super().__init__(identifier=identifier, measurements=measurements)
+ super().__init__(identifier=identifier, measurements=measurements, metadata=metadata)
if arithmetic_operator not in ArithmeticWaveform.operator_map:
raise ValueError('Unknown operator. allowed: %r' % set(ArithmeticWaveform.operator_map.keys()))
@@ -67,11 +74,13 @@ def __init__(self,
"If they evaluate to different values on instantiation this will result in an error. "
"(%r != %r) for ALL inputs "
"(it may be unequal only for fringe cases)" % (lhs.duration, rhs.duration),
+ stacklevel=2,
category=UnequalDurationWarningInArithmeticPT)
if not silent_atomic and not (lhs._is_atomic() and rhs._is_atomic()):
warnings.warn("ArithmeticAtomicPulseTemplate treats all operands as if they are atomic. "
"You can silence this warning by passing `silent_atomic=True` or by ignoring this category.",
+ stacklevel=2,
category=ImplicitAtomicityInArithmeticPT)
self._lhs = lhs
@@ -197,21 +206,24 @@ def __init__(self,
arithmetic_operator: str,
rhs: Union[PulseTemplate, ExpressionLike, Mapping[ChannelID, ExpressionLike]],
*,
- identifier: Optional[str] = None):
- """Implements the arithmetics between an aribrary pulse template and scalar values. The values can be the same
+ identifier: Optional[str] = None,
+ registry: PulseRegistryType = None,
+ metadata: TemplateMetadata | dict = None,
+ ):
+ """Implements the arithmetics between an arbitrary pulse template and scalar values. The values can be the same
for all channels, channel specific or only for a subset of the inner pulse templates defined channels.
- The expression may be time dependent if the pulse template is atomic.
+ The expression may be time-dependent if the pulse template is atomic.
- A channel dependent scalar is represented by a mapping of ChannelID -> Expression.
+ A channel dependent scalar is represented by a :py:class:`.Mapping[ChannelID, Expression]`.
The allowed operations are:
- scalar + pulse_template
- scalar - pulse_template
- scalar * pulse_template
- pulse_template + scalar
- pulse_template - scalar
- pulse_template * scalar
- pulse_template / scalar
+ - ``scalar + pulse_template``
+ - ``scalar - pulse_template``
+ - ``scalar * pulse_template``
+ - ``pulse_template + scalar``
+ - ``pulse_template - scalar``
+ - ``pulse_template * scalar``
+ - ``pulse_template / scalar``
Args:
lhs: Left hand side operand
@@ -224,7 +236,7 @@ def __init__(self,
and a composite pulse template.
ValueError: If the scalar is a mapping and contains channels that are not defined on the pulse template.
"""
- PulseTemplate.__init__(self, identifier=identifier)
+ PulseTemplate.__init__(self, identifier=identifier, metadata=metadata)
if not isinstance(lhs, PulseTemplate) and not isinstance(rhs, PulseTemplate):
raise TypeError('At least one of the operands needs to be a pulse template.')
@@ -263,6 +275,8 @@ def __init__(self,
# this is a hack so we can use the AtomicPulseTemplate.integral default implementation
self._AS_EXPRESSION_TIME = AtomicPulseTemplate._AS_EXPRESSION_TIME
+ self._register(registry=registry)
+
@staticmethod
def _parse_operand(operand: Union[ExpressionLike, Mapping[ChannelID, ExpressionLike]],
channels: Set[ChannelID]) -> Union[ExpressionScalar, Mapping[ChannelID, ExpressionScalar]]:
@@ -373,29 +387,17 @@ def _get_transformation(self,
ScalingTransformation(scalar_value)
)
- def _internal_create_program(self, *,
- scope: Scope,
- measurement_mapping: Dict[str, Optional[str]],
- channel_mapping: Dict[ChannelID, Optional[ChannelID]],
- global_transformation: Optional[Transformation],
- to_single_waveform: Set[Union[str, 'PulseTemplate']],
- parent_loop: 'Loop'):
- """The operation is applied by modifying the transformation the pulse template operand sees."""
+ def _internal_build_program(self, program_builder: ProgramBuilder):
+ build_context = program_builder.build_context
+ scope = build_context.scope
if not scope.get_volatile_parameters().keys().isdisjoint(self._scalar_operand_parameters):
raise NotImplementedError('The scalar operand of arithmetic pulse template cannot be volatile')
- # put arithmetic into transformation
inner_transformation = self._get_transformation(parameters=scope,
- channel_mapping=channel_mapping)
+ channel_mapping=build_context.channel_mapping)
+ with program_builder.with_transformation(inner_transformation):
+ self._pulse_template._build_program(program_builder=program_builder)
- transformation = inner_transformation.chain(global_transformation)
-
- self._pulse_template._create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- global_transformation=transformation,
- to_single_waveform=to_single_waveform,
- parent_loop=parent_loop)
def build_waveform(self,
parameters: Dict[str, Real],
@@ -540,8 +542,8 @@ def _is_atomic(self):
def try_operation(lhs: Union[PulseTemplate, ExpressionLike, Mapping[ChannelID, ExpressionLike]],
op: str,
rhs: Union[PulseTemplate, ExpressionLike, Mapping[ChannelID, ExpressionLike]],
- **kwargs) -> Union['ArithmeticPulseTemplate', type(NotImplemented)]:
- """
+ **kwargs) -> Union[ArithmeticPulseTemplate, ArithmeticAtomicPulseTemplate, type(NotImplemented)]:
+ """Helper implementation for arithmetic operations.
Args:
lhs: Left hand side operand
@@ -550,8 +552,10 @@ def try_operation(lhs: Union[PulseTemplate, ExpressionLike, Mapping[ChannelID, E
**kwargs: Forwarded to class init
Returns:
- ArithmeticPulseTemplate if the desired operation is valid and returns a pulse template
- NotImplemented otherwise
+ One of the following
+ - :py:class:`.ArithmeticPulseTemplate` if the desired operation is valid and returns a pulse template or
+ - :py:class:`.ArithmeticAtomicPulseTemplate` if the operands are valid atomic pulse templates or
+ - :py:class:`.NotImplemented` otherwise.
"""
try:
# returns if only one of the operands is a pulse template and the operation is valid
diff --git a/qupulse/pulses/constant_pulse_template.py b/qupulse/pulses/constant_pulse_template.py
index 24ae16847..397492692 100644
--- a/qupulse/pulses/constant_pulse_template.py
+++ b/qupulse/pulses/constant_pulse_template.py
@@ -1,16 +1,16 @@
-"""This module defines the ConstantPulseTemplate, a pulse tempalte representating a pulse with constant values on all channels
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
-Classes:
- - ConstantPulseTemplate: Defines a pulse via channel-value pairs
- - ConstantPulseWaveform: A waveform instantiated from a TablePulseTemplate by providing values for its
- declared parameters.
+"""This module defines the ConstantPulseTemplate, a pulse template representing a pulse with constant values on all channels.
"""
import logging
import numbers
from typing import Any, Dict, List, Optional, Union, Mapping, AbstractSet
-from qupulse._program.waveforms import ConstantWaveform
+from qupulse.program.waveforms import ConstantWaveform
+from qupulse.pulses.metadata import TemplateMetadata
from qupulse.utils.types import TimeType, ChannelID
from qupulse.utils import cached_property
from qupulse.expressions import ExpressionScalar, ExpressionLike
@@ -18,23 +18,32 @@
from qupulse.pulses.pulse_template import AtomicPulseTemplate, MeasurementDeclaration
from qupulse.serialization import PulseRegistryType
+
__all__ = ["ConstantPulseTemplate"]
class ConstantPulseTemplate(AtomicPulseTemplate): # type: ignore
- def __init__(self, duration: ExpressionLike, amplitude_dict: Dict[ChannelID, ExpressionLike],
+ def __init__(self, duration: ExpressionLike,
+ amplitude_dict: Dict[ChannelID, ExpressionLike],
identifier: Optional[str] = None,
name: Optional[str] = None,
measurements: Optional[List[MeasurementDeclaration]] = None,
- registry: PulseRegistryType=None) -> None:
+ registry: PulseRegistryType=None,
+ metadata: TemplateMetadata | dict = None
+ ) -> None:
"""An atomic pulse template qupulse representing a multi-channel pulse with constant values.
+
+ As an optimization, this class does not convert plain floats or ints to qupulse expressions.
Args:
duration: Duration of the template
amplitude_dict: Dictionary with values for the channels
+ identifier: Optional identifier for the pulse
name: Name for the template. Not used by qupulse
+ measurements: Passed to :py:class:`.MeasurementDefiner` superclass
+ registry: The pulse is registered in this mapping after construction if an identifier is provided
"""
- super().__init__(identifier=identifier, measurements=measurements)
+ super().__init__(identifier=identifier, measurements=measurements, metadata=metadata)
# we special case numeric values in this PulseTemplate for performance reasons
self._duration = duration if isinstance(duration, (float, int, TimeType)) else ExpressionScalar(duration)
diff --git a/qupulse/pulses/function_pulse_template.py b/qupulse/pulses/function_pulse_template.py
index fa02feaff..8796a1810 100644
--- a/qupulse/pulses/function_pulse_template.py
+++ b/qupulse/pulses/function_pulse_template.py
@@ -1,8 +1,9 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
"""This module defines the FunctionPulseTemplate, one of the elementary pulse templates and its
waveform representation.
-
-Classes:
- - FunctionPulseTemplate: Defines a pulse via a mathematical function.
"""
@@ -12,12 +13,13 @@
import sympy
from qupulse.expressions import ExpressionScalar
+from qupulse.pulses.metadata import TemplateMetadata
from qupulse.serialization import Serializer, PulseRegistryType
from qupulse.utils.types import ChannelID, TimeType, time_from_float
from qupulse.pulses.parameters import ParameterConstrainer, ParameterConstraint
from qupulse.pulses.pulse_template import AtomicPulseTemplate, MeasurementDeclaration
-from qupulse._program.waveforms import FunctionWaveform
+from qupulse.program.waveforms import FunctionWaveform
__all__ = ["FunctionPulseTemplate"]
@@ -43,9 +45,10 @@ def __init__(self,
*,
measurements: Optional[List[MeasurementDeclaration]]=None,
parameter_constraints: Optional[List[Union[str, ParameterConstraint]]]=None,
- registry: PulseRegistryType=None) -> None:
- """Creates a new FunctionPulseTemplate object.
-
+ registry: PulseRegistryType=None,
+ metadata: TemplateMetadata | dict = None,
+ ) -> None:
+ """
Args:
expression: The function represented by this FunctionPulseTemplate
as a mathematical expression where 't' denotes the time variable and other variables
@@ -58,9 +61,10 @@ def __init__(self,
measurements: A list of measurement declarations forwarded to the
:class:`~qupulse.pulses.measurement.MeasurementDefiner` superclass
parameter_constraints: A list of parameter constraints forwarded to the
- :class:`~qupulse.pulses.measurement.ParameterConstrainer` superclass
+ :py:class:`~qupulse.pulses.measurement.ParameterConstrainer` superclass
+ registry: After initialization this pulse is registered in the given mapping if an identifier is provided.
"""
- AtomicPulseTemplate.__init__(self, identifier=identifier, measurements=measurements)
+ AtomicPulseTemplate.__init__(self, identifier=identifier, measurements=measurements, metadata=metadata)
ParameterConstrainer.__init__(self, parameter_constraints=parameter_constraints)
self.__expression = ExpressionScalar.make(expression)
diff --git a/qupulse/pulses/interpolation.py b/qupulse/pulses/interpolation.py
index 40eac69e9..a5646f007 100644
--- a/qupulse/pulses/interpolation.py
+++ b/qupulse/pulses/interpolation.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
"""This module defines strategies for interpolation between points in a pulse table or similar.
Classes:
diff --git a/qupulse/pulses/loop_pulse_template.py b/qupulse/pulses/loop_pulse_template.py
index f47520a73..15fde39cc 100644
--- a/qupulse/pulses/loop_pulse_template.py
+++ b/qupulse/pulses/loop_pulse_template.py
@@ -1,5 +1,12 @@
-"""This module defines LoopPulseTemplate, a higher-order hierarchical pulse template that loops
-another PulseTemplate based on a condition."""
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+"""
+This module defines LoopPulseTemplate, a higher-order hierarchical pulse template that loops its body PulseTemplate.
+
+"""
+import dataclasses
import functools
import itertools
from abc import ABC
@@ -13,13 +20,14 @@
from qupulse.parameter_scope import Scope, MappedScope, DictScope
from qupulse.utils.types import FrozenDict, FrozenMapping
-from qupulse._program._loop import Loop
+from qupulse.program import ProgramBuilder
from qupulse.expressions import ExpressionScalar, ExpressionVariableMissingException, Expression
from qupulse.utils import checked_int_cast, cached_property
from qupulse.pulses.parameters import InvalidParameterNameException, ParameterConstrainer, ParameterNotProvidedException
-from qupulse.pulses.pulse_template import PulseTemplate, ChannelID, AtomicPulseTemplate
-from qupulse._program.waveforms import SequenceWaveform as ForLoopWaveform
+from qupulse.pulses.pulse_template import PulseTemplate, ChannelID
+from qupulse.pulses.metadata import TemplateMetadata
+from qupulse.program.waveforms import SequenceWaveform as ForLoopWaveform
from qupulse.pulses.measurement import MeasurementDefiner, MeasurementDeclaration
from qupulse.pulses.range import ParametrizedRange, RangeScope
@@ -29,8 +37,9 @@
class LoopPulseTemplate(PulseTemplate):
"""Base class for loop based pulse templates. This class is still abstract and cannot be instantiated."""
def __init__(self, body: PulseTemplate,
- identifier: Optional[str]):
- super().__init__(identifier=identifier)
+ identifier: Optional[str],
+ metadata: Union[TemplateMetadata, dict] = None):
+ super().__init__(identifier=identifier, metadata=metadata)
self.__body = body
@property
@@ -47,9 +56,15 @@ def measurement_names(self) -> Set[str]:
class ForLoopPulseTemplate(LoopPulseTemplate, MeasurementDefiner, ParameterConstrainer):
- """This pulse template allows looping through an parametrized integer range and provides the loop index as a
+ """
+ This pulse template allows looping through a parametrized integer range and provides the loop index as a
parameter to the body. If you do not need the index in the pulse template, consider using
- :class:`~qupulse.pulses.repetition_pulse_template.RepetitionPulseTemplate`"""
+ :py:class:`~qupulse.pulses.repetition_pulse_template.RepetitionPulseTemplate`.
+
+ Besides direct creation, you can construct this pulse template with the :py:meth:`.PulseTemplate.with_iteration`
+ helper method.
+
+ """
def __init__(self,
body: PulseTemplate,
loop_index: str,
@@ -63,15 +78,27 @@ def __init__(self,
*,
measurements: Optional[Sequence[MeasurementDeclaration]]=None,
parameter_constraints: Optional[Sequence]=None,
+ metadata: Union[TemplateMetadata, dict] = None,
registry: PulseRegistryType=None) -> None:
"""
+ You can associate an identifier, measurements and parameter constraints with this pulse template. If the pulse template
+ does not result in any output (for example, because the range evaluates to an empty range or the body is empty at runtime)
+ the measurements are dropped as well.
+
Args:
- body: The loop body. It is expected to have `loop_index` as an parameter
- loop_index: Loop index of the for loop
- loop_range: Range to loop through
- identifier: Used for serialization
+ body: The loop body. It is expected to have ``loop_index`` as a parameter.
+ loop_index: Loop index of the loop.
+ loop_range: Range to loop through. Used to construct a :py:class:`.ParametrizedRange`.
+ identifier: Used for serialization and the pulse registry
+ measurements: Measurements passed to :py:class:`~qupulse.pulses.measurement.MeasurementDefiner` superclass
+ parameter_constraints: Constraints passed to :py:class:`~qupulse.pulses.pulse_template.ParameterConstrainer` superclass
+ registry: The new pulse is registered there after initialization.
+ metadata: Used to initialize :py:attr:`.PulseTemplate.metadata`
+
+ Raises:
+ :py:class:`.LoopIndexNotUsedException` if the loop index is no parameter of the body
"""
- LoopPulseTemplate.__init__(self, body=body, identifier=identifier)
+ LoopPulseTemplate.__init__(self, body=body, identifier=identifier, metadata=metadata)
MeasurementDefiner.__init__(self, measurements=measurements)
ParameterConstrainer.__init__(self, parameter_constraints=parameter_constraints)
@@ -130,7 +157,7 @@ def duration(self) -> ExpressionScalar:
@property
def parameter_names(self) -> Set[str]:
- parameter_names = self.body.parameter_names.copy()
+ parameter_names = set(self.body.parameter_names)
parameter_names.remove(self._loop_index)
return parameter_names | self._loop_range.parameter_names | self.constrained_parameters | self.measurement_parameters
@@ -143,32 +170,15 @@ def _body_scope_generator(self, scope: Scope, forward=True) -> Iterator[Scope]:
for loop_index_value in loop_range:
yield _ForLoopScope(scope, loop_index_name, loop_index_value)
- def _internal_create_program(self, *,
- scope: Scope,
- measurement_mapping: Dict[str, Optional[str]],
- channel_mapping: Dict[ChannelID, Optional[ChannelID]],
- global_transformation: Optional['Transformation'],
- to_single_waveform: Set[Union[str, 'PulseTemplate']],
- parent_loop: Loop) -> None:
- self.validate_scope(scope=scope)
-
- try:
- duration = self.duration.evaluate_in_scope(scope)
- except ExpressionVariableMissingException as err:
- raise ParameterNotProvidedException(err.variable) from err
-
- if duration > 0:
- measurements = self.get_measurement_windows(scope, measurement_mapping)
- if measurements:
- parent_loop.add_measurements(measurements)
-
- for local_scope in self._body_scope_generator(scope, forward=True):
- self.body._create_program(scope=local_scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- global_transformation=global_transformation,
- to_single_waveform=to_single_waveform,
- parent_loop=parent_loop)
+ def _internal_build_program(self, program_builder: ProgramBuilder):
+ build_context = program_builder.build_context
+ scope = build_context.scope
+ loop_range = self._loop_range.to_range(scope)
+ loop_index_name = self._loop_index
+
+ measurements = self.get_measurement_windows(scope, build_context.measurement_mapping)
+ for iteration_program_builder in program_builder.with_iteration(loop_index_name, loop_range, measurements=measurements):
+ self.body._build_program(program_builder=iteration_program_builder)
def build_waveform(self, parameter_scope: Scope) -> ForLoopWaveform:
return ForLoopWaveform([self.body.build_waveform(local_scope)
@@ -255,3 +265,9 @@ def __str__(self) -> str:
_ForLoopScope = RangeScope
+
+
+@dataclasses.dataclass
+class _ForLoopIndexValue:
+ name: str
+ rng: range
diff --git a/qupulse/pulses/mapping_pulse_template.py b/qupulse/pulses/mapping_pulse_template.py
index 9134da251..af768f599 100644
--- a/qupulse/pulses/mapping_pulse_template.py
+++ b/qupulse/pulses/mapping_pulse_template.py
@@ -1,16 +1,25 @@
-from typing import Optional, Set, Dict, Union, List, Any, Tuple
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+"""
+Defines the pulse template for parameter, channel and measurement mapping.
+"""
+
+from typing import Optional, Set, Dict, Union, List, Any, Tuple, Mapping
import itertools
import numbers
import collections
from qupulse.utils.types import ChannelID, FrozenDict, FrozenMapping
-from qupulse.expressions import Expression, ExpressionScalar
+from qupulse.expressions import Expression, ExpressionScalar, ExpressionLike
from qupulse.parameter_scope import Scope, MappedScope
from qupulse.pulses.pulse_template import PulseTemplate, MappingTuple
+from qupulse.pulses.metadata import SingleWaveformStrategy, TemplateMetadata
from qupulse.pulses.parameters import ParameterNotProvidedException, ParameterConstrainer
-from qupulse._program.waveforms import Waveform
-from qupulse._program._loop import Loop
-from qupulse.serialization import Serializer, PulseRegistryType
+from qupulse.program.waveforms import Waveform
+from qupulse.program import ProgramBuilder
+from qupulse.serialization import Serializer, PulseRegistryType, SerializableMeta
__all__ = [
"MappingPulseTemplate",
@@ -20,39 +29,64 @@
class MappingPulseTemplate(PulseTemplate, ParameterConstrainer):
- """This class can be used to remap parameters, the names of measurement windows and the names of channels. Besides
- the standard constructor, there is a static member function from_tuple for convenience. The class also allows
- constraining parameters by deriving from ParameterConstrainer"""
+ """This class can be used to map a pulse template's parameters with mathematical expressions and rename its measurements and channels.
+
+ Besides the standard constructor which is intended for verbose construction with keyword arguments,
+ there is :py:meth:`.MappingPulseTemplate.from_tuple` which allows compact code style.
+ It is also used by the convenience function :py:meth:`.PulseTemplate.with_mapping`.
+ The class allows constraining the newly mapped parameters by deriving from :py:class:`.ParameterConstrainer`.
+ """
+
+ #: Default value for ``allow_partial_parameter_mapping`` of the initialization.
ALLOW_PARTIAL_PARAMETER_MAPPING = True
- """Default value for allow_partial_parameter_mapping of the __init__ method."""
def __init__(self, template: PulseTemplate, *,
identifier: Optional[str]=None,
- parameter_mapping: Optional[Dict[str, str]]=None,
- measurement_mapping: Optional[Dict[str, str]] = None,
- channel_mapping: Optional[Dict[ChannelID, ChannelID]] = None,
+ parameter_mapping: Optional[Dict[str, ExpressionLike]]=None,
+ measurement_mapping: Optional[Dict[str, Optional[str]]] = None,
+ channel_mapping: Optional[Dict[ChannelID, Optional[ChannelID]]] = None,
parameter_constraints: Optional[List[str]]=None,
allow_partial_parameter_mapping: bool = None,
+ metadata: Union[TemplateMetadata, dict]=None,
registry: PulseRegistryType=None) -> None:
- """Standard constructor for the MappingPulseTemplate.
-
- Mappings that are not specified are defaulted to identity mappings. Channels and measurement names of the
- encapsulated template can be mapped partially by default. F.i. if channel_mapping only contains one of two
- channels the other channel name is mapped to itself. Channels that are mapped to None are dropped.
- However, if a parameter mapping is specified and one or more parameters are not mapped a MissingMappingException
- is raised. To allow partial mappings and enable the same behaviour as for the channel and measurement name
- mapping allow_partial_parameter_mapping must be set to True.
- Furthermore parameter constrains can be specified.
-
- :param template: The encapsulated pulse template whose parameters, measurement names and channels are mapped
- :param parameter_mapping: if not none, mappings for all parameters must be specified
- :param measurement_mapping: mappings for other measurement names are inserted
- :param channel_mapping: mappings for other channels are auto inserted. Mapping to None drops the channel.
- :param parameter_constraints:
- :param allow_partial_parameter_mapping: If None the value of the class variable ALLOW_PARTIAL_PARAMETER_MAPPING
"""
- PulseTemplate.__init__(self, identifier=identifier)
+ Mappings that are not specified are defaulted to identity mappings.
+ Channels and measurement names of the encapsulated template can be mapped partially by default.
+ For example, if ``channel_mapping`` only contains one of two channels the other channel name is mapped to itself.
+
+ >>> from qupulse.pulses import *
+ >>> inner = ConstantPT(duration=1, amplitude_dict={'A': 1.0, 'B': 2.0})
+ >>> inner.defined_channels
+ {'A', 'B'}
+ >>> mapped = MappingPT(inner, channel_mapping={'A': 'X'})
+ >>> mapped.defined_channels
+ {'X', 'B'}
+
+ Channels that are mapped to None are dropped.
+
+ >>> mapped = MappingPT(inner, channel_mapping={'A': None})
+ >>> mapped.defined_channels
+ {'B'}
+
+ However, ``allow_partial_parameter_mapping`` is set False or if it is unset and the default value
+ :py:attr:`MappingPulseTemplate.ALLOW_PARTIAL_PARAMETER_MAPPING` is used, the parameter mappings must map all parameters.
+ Otherwise, a :py:class:`.MissingMappingException` is raised.
+
+ Raises:
+ ValueError: If the channel mapping maps multiple channels to the same channel.
+
+ Args:
+ template: The encapsulated pulse template whose parameters, measurement names and channels are mapped
+ parameter_mapping: if not none, mappings for all parameters must be specified
+ measurement_mapping: mappings for other measurement names are inserted
+ channel_mapping: mappings for other channels are auto inserted. Mapping to None drops the channel.
+ parameter_constraints: See :py:class:`.ParameterConstrainer`
+ allow_partial_parameter_mapping: If None it defaults to :py:attr:`MappingPulseTemplate.ALLOW_PARTIAL_PARAMETER_MAPPING`
+ metadata: Used to initialize :py:attr:`.PulseTemplate.metadata`.
+ registry: If specified, the new pulse template is registered there after initialization.
+ """
+ PulseTemplate.__init__(self, identifier=identifier, metadata=metadata)
ParameterConstrainer.__init__(self, parameter_constraints=parameter_constraints)
if allow_partial_parameter_mapping is None:
@@ -113,7 +147,7 @@ def __init__(self, template: PulseTemplate, *,
template = template.template
self.__template: PulseTemplate = template
- self.__parameter_mapping = FrozenDict(parameter_mapping)
+ self.__parameter_mapping: Mapping[str, Expression] = FrozenDict(parameter_mapping)
self.__external_parameters = set(itertools.chain(*(expr.variables for expr in self.__parameter_mapping.values())))
self.__external_parameters |= self.constrained_parameters
self.__measurement_mapping = measurement_mapping
@@ -260,7 +294,7 @@ def map_parameter_values(self, parameters: Dict[str, numbers.Real],
A new dictionary with mapped numeric values.
"""
self._validate_parameters(parameters=parameters, volatile=volatile)
- return {parameter: mapping_function.evaluate_numeric(**parameters)
+ return {parameter: mapping_function.evaluate_in_scope(parameters)
for parameter, mapping_function in self.__parameter_mapping.items()}
def map_scope(self, scope: Scope) -> MappedScope:
@@ -286,31 +320,39 @@ def map_parameters(self,
raise TypeError('Values of parameter dict are neither all Parameter nor Real')
def get_updated_measurement_mapping(self, measurement_mapping: Dict[str, str]) -> Dict[str, str]:
+ """This function integrates this object's measurement mapping with the supplied ``measurement_mapping`` i.e., it translates
+ a mapping that is valid in the outer namespace to a mapping that is valid in the inner namespace.
+
+ Args:
+ measurement_mapping: Measurement mapping to translate.
+
+ Returns:
+ The measurement mapping translated for the inner template to consume.
+ """
return {k: measurement_mapping[v] for k, v in self.__measurement_mapping.items()}
def get_updated_channel_mapping(self, channel_mapping: Dict[ChannelID,
Optional[ChannelID]]) -> Dict[ChannelID,
Optional[ChannelID]]:
+ """This function integrates this object's channel mapping with the supplied ``channel_mapping`` i.e., it translates
+ a mapping that is valid in the outer namespace to a mapping that is valid in the inner namespace.
+
+ Args:
+ channel_mapping: Channel mapping to translate.
+
+ Returns:
+ The channel mapping translated for the inner template to consume.
+ """
# do not look up the mapped outer channel if it is None (this marks a deleted channel)
return {inner_ch: None if outer_ch is None else channel_mapping[outer_ch]
for inner_ch, outer_ch in self.__channel_mapping.items()}
- def _internal_create_program(self, *,
- scope: Scope,
- measurement_mapping: Dict[str, Optional[str]],
- channel_mapping: Dict[ChannelID, Optional[ChannelID]],
- global_transformation: Optional['Transformation'],
- to_single_waveform: Set[Union[str, 'PulseTemplate']],
- parent_loop: Loop) -> None:
- self.validate_scope(scope)
-
- # parameters are validated in map_parameters() call, no need to do it here again explicitly
- self.template._create_program(scope=self.map_scope(scope),
- measurement_mapping=self.get_updated_measurement_mapping(measurement_mapping),
- channel_mapping=self.get_updated_channel_mapping(channel_mapping),
- global_transformation=global_transformation,
- to_single_waveform=to_single_waveform,
- parent_loop=parent_loop)
+ def _internal_build_program(self, program_builder: ProgramBuilder):
+ with program_builder.with_mappings(
+ parameter_mapping=self.__parameter_mapping,
+ channel_mapping=self.__channel_mapping,
+ measurement_mapping=self.__measurement_mapping) as inner_builder:
+ self.__template._build_program(program_builder=inner_builder)
def build_waveform(self,
parameters: Dict[str, numbers.Real],
@@ -414,3 +456,6 @@ def __init__(self, template: PulseTemplate, object_type: str, mapped: Set, mappi
def __str__(self) -> str:
return self.message + '\n'.join(str(mapping) for mapping in self.mappings)
+
+# backward compatibility with very old pulse deserialization
+SerializableMeta.deserialization_callbacks["qupulse.pulses.pulse_template_parameter_mapping.MappingPulseTemplate"] = SerializableMeta.deserialization_callbacks[MappingPulseTemplate.get_type_identifier()]
diff --git a/qupulse/pulses/measurement.py b/qupulse/pulses/measurement.py
index c9d1f9ba2..4e545aa4e 100644
--- a/qupulse/pulses/measurement.py
+++ b/qupulse/pulses/measurement.py
@@ -1,8 +1,12 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
from typing import Optional, List, Tuple, Union, Dict, Set, Mapping, AbstractSet
from numbers import Real
import itertools
-from qupulse.expressions import Expression
+from qupulse.expressions import Expression, ExpressionScalar
from qupulse.utils.types import MeasurementWindow
from qupulse.parameter_scope import Scope
@@ -11,12 +15,19 @@
class MeasurementDefiner:
def __init__(self, measurements: Optional[List[MeasurementDeclaration]]):
+ """This is a superclass for pulse templates that define measurements.
+
+ Args:
+ measurements: Sequence of ``(name, begin, length)`` tuples which define which measurements to be associated
+ with this object. The ``begin`` times are relative to the beginning of the defining pulse.
+ """
+
if measurements is None:
self._measurement_windows = []
else:
self._measurement_windows = [(name,
- begin if isinstance(begin, Expression) else Expression(begin),
- length if isinstance(length, Expression) else Expression(length))
+ begin if isinstance(begin, Expression) else ExpressionScalar(begin),
+ length if isinstance(length, Expression) else ExpressionScalar(length))
for name, begin, length in measurements]
for _, _, length in self._measurement_windows:
if (length < 0) is True:
@@ -24,7 +35,7 @@ def __init__(self, measurements: Optional[List[MeasurementDeclaration]]):
def get_measurement_windows(self,
parameters: Union[Mapping[str, Real], Scope],
- measurement_mapping: Dict[str, Optional[str]]) -> List[MeasurementWindow]:
+ measurement_mapping: Mapping[str, Optional[str]]) -> List[MeasurementWindow]:
"""Calculate measurement windows with the given parameter set and rename them with the measurement mapping. This
method only returns the measurement windows that are defined on `self`. It does _not_ collect the measurement
windows defined on eventual child objects that `self` has/is composed of.
@@ -73,8 +84,8 @@ def measurement_parameters(self) -> AbstractSet[str]:
def measurement_declarations(self) -> List[MeasurementDeclaration]:
"""Return the measurements that are directly declared on `self`. Does _not_ visit eventual child objects."""
return [(name,
- begin.original_expression,
- length.original_expression)
+ begin,
+ length)
for name, begin, length in self._measurement_windows]
@property
diff --git a/qupulse/pulses/metadata.py b/qupulse/pulses/metadata.py
new file mode 100644
index 000000000..2454674e0
--- /dev/null
+++ b/qupulse/pulses/metadata.py
@@ -0,0 +1,50 @@
+import collections
+import dataclasses
+
+from typing import Literal, Optional
+
+from qupulse.utils.types import TimeType
+
+SingleWaveformStrategy = Literal['always']
+
+
+@dataclasses.dataclass(frozen=False, eq=False, repr=False)
+class TemplateMetadata:
+ """This class is used to store metadata for pulse templates.
+
+ It is the only volatile part of pulse templates and thus does not participate in it's equality operation.
+
+ It implements the serializable protocol.
+ """
+
+ to_single_waveform: Optional[SingleWaveformStrategy] = dataclasses.field(default=None)
+ minimal_sample_rate: Optional[TimeType] = dataclasses.field(default=None)
+
+ def __init__(self,
+ to_single_waveform: Optional[SingleWaveformStrategy] = None,
+ minimal_sample_rate: Optional[TimeType] = None,
+ **kwargs):
+ # TODO: generate this init automatically
+ # The reason for the custom init is that we want to allow additional kwargs
+ self.to_single_waveform = to_single_waveform
+ self.minimal_sample_rate = minimal_sample_rate
+ for key, value in kwargs.items():
+ setattr(self, key, value)
+
+ def __repr__(self):
+ args = ",".join(f"{name}={value!r}"
+ for name, value in self.get_serialization_data().items())
+ return f'{self.__class__.__name__}({args})'
+
+ def get_serialization_data(self):
+ data = vars(self).copy()
+
+ for field in dataclasses.fields(self):
+ if field.default is not dataclasses.MISSING:
+ if data[field.name] == field.default:
+ del data[field.name]
+
+ return data
+
+ def __bool__(self):
+ return bool(self.get_serialization_data())
diff --git a/qupulse/pulses/multi_channel_pulse_template.py b/qupulse/pulses/multi_channel_pulse_template.py
index beb12b68e..ace764cf5 100644
--- a/qupulse/pulses/multi_channel_pulse_template.py
+++ b/qupulse/pulses/multi_channel_pulse_template.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
"""This module defines MultiChannelPulseTemplate, which allows the combination of several
AtomicPulseTemplates into a single template spanning several channels.
@@ -11,14 +15,16 @@
import numbers
import warnings
+from qupulse.program import ProgramBuilder
+from qupulse.pulses.metadata import TemplateMetadata
from qupulse.serialization import Serializer, PulseRegistryType
from qupulse.parameter_scope import Scope
from qupulse.utils import isclose
from qupulse.utils.sympy import almost_equal, Sympifyable
from qupulse.utils.types import ChannelID, TimeType
-from qupulse._program.waveforms import MultiChannelWaveform, Waveform, TransformingWaveform
-from qupulse._program.transformation import ParallelChannelTransformation, Transformation, chain_transformations
+from qupulse.program.waveforms import MultiChannelWaveform, Waveform, TransformingWaveform
+from qupulse.program.transformation import ParallelChannelTransformation, Transformation, chain_transformations
from qupulse.pulses.pulse_template import PulseTemplate, AtomicPulseTemplate
from qupulse.pulses.mapping_pulse_template import MappingPulseTemplate, MappingTuple
from qupulse.pulses.parameters import ParameterConstrainer
@@ -35,7 +41,9 @@ def __init__(self,
parameter_constraints: Optional[List] = None,
measurements: Optional[List[MeasurementDeclaration]] = None,
registry: PulseRegistryType = None,
- duration: Optional[ExpressionLike] = None) -> None:
+ duration: Optional[ExpressionLike] = None,
+ metadata: TemplateMetadata | dict = None,
+ ) -> None:
"""Combines multiple AtomicPulseTemplates of the same duration that are defined on different channels into an
AtomicPulseTemplate.
If the duration keyword argument is given it is enforced that the instantiated pulse template has this duration.
@@ -50,7 +58,7 @@ def __init__(self,
duration: Enforced duration of the pulse template on instantiation. build_waveform checks all sub-waveforms
have this duration. If True the equality of durations is only checked durtin instantiation not construction.
"""
- AtomicPulseTemplate.__init__(self, identifier=identifier, measurements=measurements)
+ AtomicPulseTemplate.__init__(self, identifier=identifier, measurements=measurements, metadata=metadata)
ParameterConstrainer.__init__(self, parameter_constraints=parameter_constraints)
self._subtemplates = [st if isinstance(st, PulseTemplate) else MappingPulseTemplate.from_tuple(st) for st in
@@ -145,7 +153,7 @@ def build_waveform(self, parameters: Dict[str, numbers.Real],
waveform = MultiChannelWaveform.from_parallel(sub_waveforms)
if self._duration:
- expected_duration = self._duration.evaluate_numeric(**parameters)
+ expected_duration = self._duration.evaluate_in_scope(parameters)
if not isclose(expected_duration, waveform.duration):
raise ValueError('The duration does not '
@@ -222,7 +230,9 @@ def __init__(self,
template: PulseTemplate,
overwritten_channels: Mapping[ChannelID, Union[ExpressionScalar, Sympifyable]], *,
identifier: Optional[str]=None,
- registry: Optional[PulseRegistryType] = None):
+ registry: Optional[PulseRegistryType] = None,
+ metadata: TemplateMetadata | dict = None,
+ ):
"""Pulse template to add new or overwrite existing channels of a contained pulse template. The channel values
may be time dependent if the contained pulse template is atomic.
@@ -233,7 +243,7 @@ def __init__(self,
identifier: Name of the pulse template for serialization
registry: Pulse template gets registered here if not None.
"""
- super().__init__(identifier=identifier)
+ super().__init__(identifier=identifier, metadata=metadata)
self._template = template
self._overwritten_channels = {channel: ExpressionScalar(value)
@@ -265,21 +275,13 @@ def _get_overwritten_channels_values(self,
for name, value in self.overwritten_channels.items()
if channel_mapping[name] is not None}
- def _internal_create_program(self, *,
- scope: Scope,
- global_transformation: Optional[Transformation],
- channel_mapping: Dict[ChannelID, Optional[ChannelID]],
- **kwargs):
- overwritten_channels = self._get_overwritten_channels_values(parameters=scope, channel_mapping=channel_mapping)
+ def _internal_build_program(self, program_builder: ProgramBuilder):
+ context = program_builder.build_context
+ overwritten_channels = self._get_overwritten_channels_values(parameters=context.scope,
+ channel_mapping=context.channel_mapping)
transformation = ParallelChannelTransformation(overwritten_channels)
-
- if global_transformation is not None:
- transformation = chain_transformations(global_transformation, transformation)
-
- self._template._create_program(scope=scope,
- channel_mapping=channel_mapping,
- global_transformation=transformation,
- **kwargs)
+ with program_builder.with_transformation(transformation) as trafo_program_builder:
+ self._template._build_program(program_builder=trafo_program_builder)
def build_waveform(self, parameters: Dict[str, numbers.Real],
channel_mapping: Dict[ChannelID, Optional[ChannelID]]) -> Optional[Waveform]:
diff --git a/qupulse/pulses/parameters.py b/qupulse/pulses/parameters.py
index 51b20e05d..8ad60942c 100644
--- a/qupulse/pulses/parameters.py
+++ b/qupulse/pulses/parameters.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
"""This module defines parameter constriants.
"""
@@ -12,8 +16,15 @@
from qupulse.serialization import AnonymousSerializable
from qupulse.expressions import Expression
from qupulse.parameter_scope import Scope, ParameterNotProvidedException
+from qupulse.utils.sympy import sympify
-__all__ = ["ParameterNotProvidedException", "ParameterConstraintViolation", "ParameterConstraint"]
+__all__ = [
+ "ParameterNotProvidedException",
+ "ParameterConstraintViolation",
+ "ParameterConstraint",
+ "ParameterConstrainer",
+ "ConstrainedParameterIsVolatileWarning",
+]
class ParameterConstraint(AnonymousSerializable):
@@ -22,9 +33,9 @@ def __init__(self, relation: Union[str, sympy.Expr]):
super().__init__()
if isinstance(relation, str) and '==' in relation:
# The '==' operator is interpreted by sympy as exactly, however we need a symbolical evaluation
- self._expression = sympy.Eq(*sympy.sympify(relation.split('==')))
+ self._expression = sympy.Eq(*sympify(relation.split('==')))
else:
- self._expression = sympy.sympify(relation)
+ self._expression = sympify(relation)
if not isinstance(self._expression, sympy.logic.boolalg.Boolean):
raise ValueError('Constraint is not boolean')
self._expression = Expression(self._expression)
diff --git a/qupulse/pulses/plotting.py b/qupulse/pulses/plotting.py
index 2e6cd6e84..5c73228c3 100644
--- a/qupulse/pulses/plotting.py
+++ b/qupulse/pulses/plotting.py
@@ -1,269 +1,8 @@
-"""This module defines plotting functionality for instantiated PulseTemplates using matplotlib.
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
-Classes:
- - PlottingNotPossibleException.
-Functions:
- - plot: Plot a pulse using matplotlib.
-"""
-
-from typing import Dict, Tuple, Any, Optional, Set, List, Union
-from numbers import Real
-
-import numpy as np
-import warnings
-import operator
-import itertools
-
-from qupulse._program import waveforms
-from qupulse.utils.types import ChannelID, MeasurementWindow, has_type_interface
-from qupulse.pulses.pulse_template import PulseTemplate
-from qupulse._program.waveforms import Waveform
-from qupulse._program._loop import Loop, to_waveform
-
-
-__all__ = ["render", "plot", "PlottingNotPossibleException"]
-
-
-def render(program: Union[Loop],
- sample_rate: Real = 10.0,
- render_measurements: bool = False,
- time_slice: Tuple[Real, Real] = None,
- plot_channels: Optional[Set[ChannelID]] = None) -> Tuple[np.ndarray, Dict[ChannelID, np.ndarray],
- List[MeasurementWindow]]:
- """'Renders' a pulse program.
-
- Samples all contained waveforms into an array according to the control flow of the program.
-
- Args:
- program: The pulse (sub)program to render. Can be represented either by a Loop object or the more
- old-fashioned InstructionBlock.
- sample_rate: The sample rate in GHz.
- render_measurements: If True, the third return value is a list of measurement windows.
- time_slice: The time slice to be rendered. If None, the entire pulse will be shown.
- plot_channels: Only channels in this set are rendered. If None, all will.
-
- Returns:
- A tuple (times, values, measurements). times is a numpy.ndarray of dimensions sample_count where
- containing the time values. voltages is a dictionary of one numpy.ndarray of dimensions sample_count per
- defined channel containing corresponding sampled voltage values for that channel.
- measurements is a sequence of all measurements where each measurement is represented by a tuple
- (name, start_time, duration).
- """
- if has_type_interface(program, Loop):
- waveform, measurements = _render_loop(program, render_measurements=render_measurements)
- else:
- raise ValueError('Cannot render an object of type %r' % type(program), program)
-
- if waveform is None:
- return np.array([]), dict(), measurements
-
- if plot_channels is None:
- channels = waveform.defined_channels
- else:
- channels = waveform.defined_channels & plot_channels
-
- if time_slice is None:
- start_time, end_time = 0, waveform.duration
-
- elif time_slice[1] < time_slice[0] or time_slice[0] < 0 or time_slice[1] < 0:
- raise ValueError("time_slice is not valid.")
-
- else:
- start_time, end_time, *_ = time_slice
-
- # filter measurement windows
- measurements = [(name, begin, length)
- for name, begin, length in measurements
- if begin < end_time and begin + length > start_time]
-
- sample_count = (end_time - start_time) * sample_rate + 1
- if sample_count < 2:
- raise PlottingNotPossibleException(pulse=None,
- description='cannot render sequence with less than 2 data points')
- if not round(float(sample_count), 10).is_integer():
- warnings.warn(f"Sample count {sample_count} is not an integer. Will be rounded (this changes the sample rate).",
- stacklevel=2)
-
- times = np.linspace(float(start_time), float(end_time), num=int(sample_count))
- times[-1] = np.nextafter(times[-1], times[-2])
-
- voltages = {ch: waveforms._ALLOCATION_FUNCTION(times, **waveforms._ALLOCATION_FUNCTION_KWARGS)
- for ch in channels}
- for ch, ch_voltage in voltages.items():
- waveform.get_sampled(channel=ch, sample_times=times, output_array=ch_voltage)
-
- return times, voltages, measurements
-
-
-def _render_loop(loop: Loop,
- render_measurements: bool,) -> Tuple[Waveform, List[MeasurementWindow]]:
- """Transform program into single waveform and measurement windows.
- The specific implementation of render for Loop arguments."""
- waveform = to_waveform(loop)
-
- if render_measurements:
- measurement_dict = loop.get_measurement_windows()
- measurement_list = []
- for name, (begins, lengths) in measurement_dict.items():
- measurement_list.extend(zip(itertools.repeat(name), begins, lengths))
- measurements = sorted(measurement_list, key=operator.itemgetter(1))
- else:
- measurements = []
-
- return waveform, measurements
-
-
-def plot(pulse: PulseTemplate,
- parameters: Dict[str, Real]=None,
- sample_rate: Optional[Real]=10,
- axes: Any=None,
- show: bool=True,
- plot_channels: Optional[Set[ChannelID]]=None,
- plot_measurements: Optional[Set[str]]=None,
- stepped: bool=True,
- maximum_points: int=10**6,
- time_slice: Tuple[Real, Real]=None,
- **kwargs) -> Any: # pragma: no cover
- """Plots a pulse using matplotlib.
-
- The given pulse template will first be turned into a pulse program (represented by a Loop object) with the provided
- parameters. The render() function is then invoked to obtain voltage samples over the entire duration of the pulse which
- are then plotted in a matplotlib figure.
-
- Args:
- pulse: The pulse to be plotted.
- parameters: An optional mapping of parameter names to Parameter
- objects.
- sample_rate: The rate with which the waveforms are sampled for the plot in
- samples per time unit. If None, then automatically determine the sample rate (default = 10)
- axes: matplotlib Axes object the pulse will be drawn into if provided
- show: If true, the figure will be shown
- plot_channels: If specified only channels from this set will be plotted. If omitted all channels will be.
- stepped: If true pyplot.step is used for plotting
- plot_measurements: If specified measurements in this set will be plotted. If omitted no measurements will be.
- maximum_points: If the sampled waveform is bigger, it is not plotted
- time_slice: The time slice to be plotted. If None, the entire pulse will be shown.
- kwargs: Forwarded to pyplot. Overwrites other settings.
- Returns:
- matplotlib.pyplot.Figure instance in which the pulse is rendered
- Raises:
- PlottingNotPossibleException if the sequencing is interrupted before it finishes, e.g.,
- because a parameter value could not be evaluated
- all Exceptions possibly raised during sequencing
- """
- from matplotlib import pyplot as plt
-
- channels = pulse.defined_channels
-
- if parameters is None:
- parameters = dict()
-
- if sample_rate is None:
- if time_slice is None:
- duration = pulse.duration
- else:
- duration = time_slice[1]-time_slice[0]
- if duration == 0:
- sample_rate = 1
- else:
- duration_per_sample = float(duration) / 1000
- sample_rate = 1 / duration_per_sample
-
- program = pulse.create_program(parameters=parameters,
- channel_mapping={ch: ch for ch in channels},
- measurement_mapping={w: w for w in pulse.measurement_names})
-
- if program is not None:
- times, voltages, measurements = render(program,
- sample_rate,
- render_measurements=bool(plot_measurements),
- time_slice=time_slice)
- else:
- times, voltages, measurements = np.array([]), dict(), []
-
- duration = 0
- if times.size == 0:
- warnings.warn("Pulse to be plotted is empty!")
- elif times.size > maximum_points:
- # todo [2018-05-30]: since it results in an empty return value this should arguably be an exception, not just a warning
- warnings.warn(f"Sampled pulse of size {times.size} is lager than {maximum_points}",
- stacklevel=2)
- return None
- else:
- duration = times[-1]
-
- if time_slice is None:
- time_slice = (0, duration)
-
- legend_handles = []
- if axes is None:
- # plot to figure
- figure = plt.figure()
- axes = figure.add_subplot(111)
-
- if plot_channels is not None:
- voltages = {ch: voltage
- for ch, voltage in voltages.items()
- if ch in plot_channels}
-
- for ch_name, voltage in voltages.items():
- label = 'channel {}'.format(ch_name)
- if stepped:
- line, = axes.step(times, voltage, **{**dict(where='post', label=label), **kwargs})
- else:
- line, = axes.plot(times, voltage, **{**dict(label=label), **kwargs})
- legend_handles.append(line)
-
- if plot_measurements:
- measurement_dict = dict()
- for name, begin, length in measurements:
- if name in plot_measurements:
- measurement_dict.setdefault(name, []).append((begin, begin+length))
-
- color_map = plt.cm.get_cmap('plasma')
- meas_colors = {name: color_map(i/len(measurement_dict))
- for i, name in enumerate(measurement_dict.keys())}
- for name, begin_end_list in measurement_dict.items():
- for begin, end in begin_end_list:
- poly = axes.axvspan(begin, end, alpha=0.2, label=name, edgecolor='black', facecolor=meas_colors[name])
- legend_handles.append(poly)
-
- axes.legend(handles=legend_handles)
-
- max_voltage = max((max(channel, default=0) for channel in voltages.values()), default=0)
- min_voltage = min((min(channel, default=0) for channel in voltages.values()), default=0)
-
- # add some margins in the presentation
- axes.set_xlim(-0.5+time_slice[0], time_slice[1] + 0.5)
- voltage_difference = max_voltage-min_voltage
- if voltage_difference>0:
- axes.set_ylim(min_voltage - 0.1*voltage_difference, max_voltage + 0.1*voltage_difference)
- axes.set_xlabel('Time (ns)')
- axes.set_ylabel('Voltage (a.u.)')
-
- if pulse.identifier:
- axes.set_title(pulse.identifier)
-
- if show:
- with warnings.catch_warnings():
- # do not show warnings in jupyter notebook with matplotlib inline backend
- warnings.filterwarnings(action="ignore",message=".*which is a non-GUI backend, so cannot show the figure.*")
- axes.get_figure().show()
- return axes.get_figure()
-
-
-class PlottingNotPossibleException(Exception):
- """Indicates that plotting is not possible because the sequencing process did not translate
- the entire given PulseTemplate structure."""
-
- def __init__(self, pulse, description = None) -> None:
- super().__init__()
- self.pulse = pulse
- self.description = description
- def __str__(self) -> str:
- if self.description is None:
- return "Plotting is not possible. There are parameters which cannot be computed."
- else:
- return "Plotting is not possible: %s." % self.description
-
+"""Deprecated plotting location. Was moved to :py:mod:`qupulse.plotting`.
+No deprecation warning because we will keep it around forever."""
+from qupulse.plotting import *
diff --git a/qupulse/pulses/point_pulse_template.py b/qupulse/pulses/point_pulse_template.py
index b5c5cfa03..c25c51f30 100644
--- a/qupulse/pulses/point_pulse_template.py
+++ b/qupulse/pulses/point_pulse_template.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
from typing import Optional, List, Union, Set, Dict, Sequence, Any, Tuple
from numbers import Real
import itertools
@@ -6,10 +10,11 @@
import sympy
import numpy as np
+from qupulse.pulses.metadata import TemplateMetadata
from qupulse.utils.sympy import IndexedBroadcast
from qupulse.utils.types import ChannelID
from qupulse.expressions import Expression, ExpressionScalar
-from qupulse._program.waveforms import TableWaveform, TableWaveformEntry
+from qupulse.program.waveforms import TableWaveform, TableWaveformEntry
from qupulse.pulses.parameters import ParameterConstraint, ParameterConstrainer
from qupulse.pulses.pulse_template import AtomicPulseTemplate, MeasurementDeclaration
from qupulse.pulses.table_pulse_template import TableEntry, EntryInInit
@@ -26,8 +31,8 @@
class PointPulseEntry(TableEntry):
def instantiate(self, parameters: Dict[str, numbers.Real], num_channels: int) -> Sequence[PointWaveformEntry]:
- t = self.t.evaluate_numeric(**parameters)
- vs = self.v.evaluate_numeric(**parameters)
+ t = self.t.evaluate_in_scope(parameters)
+ vs = self.v.evaluate_in_scope(parameters)
if isinstance(vs, numbers.Number):
vs = (vs,) * num_channels
@@ -46,9 +51,11 @@ def __init__(self,
parameter_constraints: Optional[List[Union[str, ParameterConstraint]]]=None,
measurements: Optional[List[MeasurementDeclaration]]=None,
identifier: Optional[str]=None,
- registry: PulseRegistryType=None) -> None:
+ registry: PulseRegistryType=None,
+ metadata: TemplateMetadata | dict = None,
+ ) -> None:
- AtomicPulseTemplate.__init__(self, identifier=identifier, measurements=measurements)
+ AtomicPulseTemplate.__init__(self, identifier=identifier, measurements=measurements, metadata=metadata)
ParameterConstrainer.__init__(self, parameter_constraints=parameter_constraints)
self._channels = tuple(channel_names)
@@ -71,7 +78,7 @@ def build_waveform(self,
for channel in self.defined_channels):
return None
- if self.duration.evaluate_numeric(**parameters) == 0:
+ if self.duration.evaluate_in_scope(parameters) == 0:
return None
mapped_channels = tuple(channel_mapping[c] for c in self._channels)
diff --git a/qupulse/pulses/pulse_template.py b/qupulse/pulses/pulse_template.py
index 46dd5294f..5fff90b58 100644
--- a/qupulse/pulses/pulse_template.py
+++ b/qupulse/pulses/pulse_template.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
"""This module defines the abstract PulseTemplate class which is the basis of any
pulse model in the qupulse.
@@ -8,25 +12,28 @@
"""
import warnings
from abc import abstractmethod
-from typing import Dict, Tuple, Set, Optional, Union, List, Callable, Any, Generic, TypeVar, Mapping
-import itertools
+from typing import Dict, Tuple, Set, Optional, Union, List, Callable, Any, Mapping, Literal
import collections
from numbers import Real, Number
+import numpy
import sympy
-from qupulse.utils.types import ChannelID, DocStringABCMeta, FrozenDict
+from qupulse.pulses.metadata import TemplateMetadata, SingleWaveformStrategy
+from qupulse.utils.types import ChannelID, FrozenDict
from qupulse.utils import forced_hash
from qupulse.serialization import Serializable
from qupulse.expressions import ExpressionScalar, Expression, ExpressionLike
-from qupulse._program._loop import Loop, to_waveform
-from qupulse._program.transformation import Transformation, IdentityTransformation, ChainedTransformation, chain_transformations
+from qupulse.program.transformation import Transformation
-from qupulse._program.waveforms import Waveform, TransformingWaveform
+from qupulse.program.waveforms import Waveform, TransformingWaveform, WaveformMetadata
from qupulse.pulses.measurement import MeasurementDefiner, MeasurementDeclaration
from qupulse.parameter_scope import Scope, DictScope
-__all__ = ["PulseTemplate", "AtomicPulseTemplate", "DoubleParameterNameException", "MappingTuple"]
+from qupulse.program import ProgramBuilder, default_program_builder, Program
+
+__all__ = ["PulseTemplate", "AtomicPulseTemplate", "DoubleParameterNameException", "MappingTuple",
+ "UnknownVolatileParameter"]
MappingTuple = Union[Tuple['PulseTemplate'],
@@ -49,11 +56,37 @@ class PulseTemplate(Serializable):
"""This is not stable"""
_DEFAULT_FORMAT_SPEC = 'identifier'
+ _CAST_INT_TO_INT64 = True
+
def __init__(self, *,
- identifier: Optional[str]) -> None:
+ identifier: Optional[str],
+ metadata: Union[TemplateMetadata, dict] = None) -> None:
super().__init__(identifier=identifier)
+ if isinstance(metadata, dict):
+ metadata = TemplateMetadata(**metadata)
+ self._metadata = metadata
+
self.__cached_hash_value = None
+ @property
+ def metadata(self) -> TemplateMetadata:
+ """The metadata is intended for information which does not concern the pulse itself but rather its usage.
+
+ Here is the place for program builder optimization hints or tags that are not targeted at qupulse. Currently,
+ qupulse itself only uses the :py:attr:`.TemplateMetadata.to_single_waveform` attribute to allow dynamic atomicity during translation.
+
+ As it is no property of the pulse itself, it is ignored for hashing and equality checks.
+ """
+ if (metadata := self._metadata) is None:
+ self._metadata = metadata = TemplateMetadata()
+ return metadata
+
+ def get_serialization_data(self, serializer: Optional['Serializer'] = None) -> Dict[str, Any]:
+ data = super().get_serialization_data(serializer=serializer)
+ if self._metadata is not None and (metadata := self._metadata.get_serialization_data()):
+ data["metadata"] = metadata
+ return data
+
@property
@abstractmethod
def parameter_names(self) -> Set[str]:
@@ -81,7 +114,18 @@ def num_channels(self) -> int:
def _is_atomic(self) -> bool:
"""This is (currently a private) a check if this pulse template always is translated into a single waveform."""
- return False
+ return self.metadata.to_single_waveform == 'always'
+
+ @property
+ def to_single_waveform(self) -> Optional[SingleWaveformStrategy]:
+ """This property describes whether this pulse template is translated into a single waveform.
+
+ 'always': It is always translated into a single waveform.
+ None: It depends on the `create_program` arguments and the pulse template itself.
+ """
+ warnings.warn("to_single_waveform is deprecated. Use metadata.to_single_waveform instead.",
+ category=DeprecationWarning, stacklevel=2)
+ return self.metadata.to_single_waveform
def __matmul__(self, other: Union['PulseTemplate', MappingTuple]) -> 'SequencePulseTemplate':
"""This method enables using the @-operator (intended for matrix multiplication) for
@@ -95,6 +139,10 @@ def __rmatmul__(self, other: MappingTuple) -> 'SequencePulseTemplate':
return SequencePulseTemplate.concatenate(other, self)
+ def __pow__(self, power: ExpressionLike):
+ """This is a convenience wrapper for :func:`.with_repetition`."""
+ return self.with_repetition(power)
+
@property
@abstractmethod
def integral(self) -> Dict[ChannelID, ExpressionScalar]:
@@ -116,7 +164,8 @@ def create_program(self, *,
channel_mapping: Optional[Mapping[ChannelID, Optional[ChannelID]]]=None,
global_transformation: Optional[Transformation]=None,
to_single_waveform: Set[Union[str, 'PulseTemplate']]=None,
- volatile: Set[str] = None) -> Optional['Loop']:
+ volatile: Union[Set[str], str] = None,
+ program_builder: ProgramBuilder = None) -> Optional[Program]:
"""Translates this PulseTemplate into a program Loop.
The returned Loop represents the PulseTemplate with all parameter values instantiated provided as dictated by
@@ -131,6 +180,8 @@ def create_program(self, *,
to_single_waveform: A set of pulse templates (or identifiers) which are directly translated to a
waveform. This might change how transformations are applied. TODO: clarify
volatile: Everything in the final program that depends on these parameters is marked as volatile
+ program_builder: This program builder is used to build the return value. If `None` `default_program_builder`
+ is used.
Returns:
A Loop object corresponding to this PulseTemplate.
"""
@@ -144,6 +195,12 @@ def create_program(self, *,
to_single_waveform = set()
if volatile is None:
volatile = set()
+ elif isinstance(volatile, str):
+ volatile = {volatile}
+ else:
+ volatile = set(volatile)
+ if program_builder is None:
+ program_builder = default_program_builder()
# make sure all channels are mapped
complete_channel_mapping = {channel: channel for channel in self.defined_channels}
@@ -161,47 +218,61 @@ def create_program(self, *,
scope = parameters
else:
parameters = dict(parameters)
+ to_int = numpy.int64 if self._CAST_INT_TO_INT64 else lambda x: x
for parameter_name, value in parameters.items():
- if not isinstance(value, Number):
+ if type(value) is int:
+ # numpy casts ints to int32 per default on windows
+ # this can easily lead to overflows when times of the order of seconds
+ # are represented with integers
+ parameters[parameter_name] = to_int(value)
+
+ elif not isinstance(value, Number):
parameters[parameter_name] = Expression(value).evaluate_numeric()
scope = DictScope(values=FrozenDict(parameters), volatile=volatile)
- root_loop = Loop()
+ for volatile_name in scope.get_volatile_parameters():
+ if volatile_name not in scope:
+ warnings.warn(f"The volatile parameter {volatile_name!r} is not in the given parameters.",
+ category=UnknownVolatileParameter,
+ stacklevel=2)
+
+ program_builder.override(
+ scope=scope,
+ measurement_mapping=measurement_mapping,
+ channel_mapping=complete_channel_mapping,
+ global_transformation=global_transformation,
+ to_single_waveform=to_single_waveform,
+ )
# call subclass specific implementation
- self._create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=complete_channel_mapping,
- global_transformation=global_transformation,
- to_single_waveform=to_single_waveform,
- parent_loop=root_loop)
-
- if root_loop.waveform is None and len(root_loop.children) == 0:
- return None # return None if no program
- return root_loop
-
- @abstractmethod
- def _internal_create_program(self, *,
- scope: Scope,
- measurement_mapping: Dict[str, Optional[str]],
- channel_mapping: Dict[ChannelID, Optional[ChannelID]],
- global_transformation: Optional[Transformation],
- to_single_waveform: Set[Union[str, 'PulseTemplate']],
- parent_loop: Loop) -> None:
- """The subclass specific implementation of create_program().
-
- Receives a Loop instance parent_loop to which it should append measurements and its own Loops as children.
-
- Subclasses should not overwrite create_program() directly but provide their implementation here. This method
- is called by create_program().
- Implementations should not call create_program() of any subtemplates to obtain Loop objects for them but
- call subtemplate._internal_create_program() instead, providing an adequate parent_loop object to which
- the subtemplate will append. Implementations must make sure not to append invalid Loop objects (no waveform or no children).
-
- In case of an error (e.g. invalid measurement mapping, missing parameters, violated parameter constraints, etc),
- implementations of this method must throw an adequate exception. They do not have to ensure that the parent_loop
- remains unchanged in this case."""
+ self._build_program(program_builder=program_builder)
+ return program_builder.to_program()
+
+ def _build_program(self, program_builder: ProgramBuilder):
+ """New method that keeps state in program builder"""
+ if (validate_scope := getattr(self, "validate_scope", None)) is not None:
+ validate_scope(program_builder.build_context.scope)
+
+ to_single_waveform = program_builder.build_settings.to_single_waveform
+ if self.metadata.to_single_waveform == 'always' or self.identifier in to_single_waveform or self in to_single_waveform:
+ with program_builder.new_subprogram() as inner_program_builder:
+ self._internal_build_program(inner_program_builder)
+ else:
+ self._internal_build_program(program_builder)
+
+ def _internal_build_program(self, program_builder: ProgramBuilder):
+ """The subclass specific implementation of create_program()."""
+ context = program_builder.build_context
+ settings = program_builder.build_settings
+ self._internal_create_program(
+ scope=context.scope,
+ measurement_mapping=dict(context.measurement_mapping),
+ channel_mapping=dict(context.channel_mapping),
+ global_transformation=context.transformation,
+ to_single_waveform=set(settings.to_single_waveform),
+ program_builder=program_builder
+ )
def _create_program(self, *,
scope: Scope,
@@ -209,36 +280,25 @@ def _create_program(self, *,
channel_mapping: Dict[ChannelID, Optional[ChannelID]],
global_transformation: Optional[Transformation],
to_single_waveform: Set[Union[str, 'PulseTemplate']],
- parent_loop: Loop):
+ program_builder: ProgramBuilder):
"""Generic part of create program. This method handles to_single_waveform and the configuration of the
transformer."""
- if self.identifier in to_single_waveform or self in to_single_waveform:
- root = Loop()
-
- if not scope.get_volatile_parameters().keys().isdisjoint(self.parameter_names):
- raise NotImplementedError('A pulse template that has volatile parameters cannot be transformed into a '
- 'single waveform yet.')
-
- self._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- global_transformation=None,
- to_single_waveform=to_single_waveform,
- parent_loop=root)
-
- waveform = to_waveform(root)
-
- if global_transformation:
- waveform = TransformingWaveform.from_transformation(waveform, global_transformation)
-
- # convert the nicely formatted measurement windows back into the old format again :(
- measurements = root.get_measurement_windows()
- measurement_window_list = []
- for measurement_name, (begins, lengths) in measurements.items():
- measurement_window_list.extend(zip(itertools.repeat(measurement_name), begins, lengths))
-
- parent_loop.add_measurements(measurement_window_list)
- parent_loop.append_child(waveform=waveform)
+ warnings.warn("_create_program is deprecated. "
+ "Update your driver to use _build_program",
+ DeprecationWarning, stacklevel=2)
+ if self.metadata.to_single_waveform == 'always' or self.identifier in to_single_waveform or self in to_single_waveform:
+ with program_builder.new_subprogram(global_transformation=global_transformation) as inner_program_builder:
+
+ if not scope.get_volatile_parameters().keys().isdisjoint(self.parameter_names):
+ raise NotImplementedError('A pulse template that has volatile parameters cannot be transformed into a '
+ 'single waveform yet.')
+
+ self._internal_create_program(scope=scope,
+ measurement_mapping=measurement_mapping,
+ channel_mapping=channel_mapping,
+ global_transformation=None,
+ to_single_waveform=to_single_waveform,
+ program_builder=inner_program_builder)
else:
self._internal_create_program(scope=scope,
@@ -246,7 +306,7 @@ def _create_program(self, *,
channel_mapping=channel_mapping,
to_single_waveform=to_single_waveform,
global_transformation=global_transformation,
- parent_loop=parent_loop)
+ program_builder=program_builder)
def with_parallel_channels(self, values: Mapping[ChannelID, ExpressionLike]) -> 'PulseTemplate':
"""Create a new pulse template that sets the given channels to the corresponding values.
@@ -358,6 +418,182 @@ def with_appended(self, *appended: 'PulseTemplate'):
else:
return self
+ def with_mapped_subtemplates(self,
+ map_fn: Callable[['PulseTemplate'], 'PulseTemplate'], *,
+ identifier_map: Callable[[Optional[str]], Optional[str]] = lambda x: x,
+ always_new_template: bool = False,
+ recursion_strategy: Literal['pre', 'post', 'self'] = 'pre') -> 'PulseTemplate':
+ """Return a new pulse template with all subtemplates mapped by ``map_fn``.
+
+ If ``map_fn`` returns the same object for all subtemplates no new pulse template is created unless ``always_new_template`` is true.
+
+ If a new template is created the identifier of the old template is passed to ``ìdentifier_map`` to determine the new identifier. It is the same by default.
+
+ By default, this function visits all subtemplates recursively. The ``recursion_strategy`` determines the order of recursion and ``map_fn`` calls.
+ For the default 'pre' the recursive subtemplates are mapped first before the direct subtemplates are mapped.
+ For 'post' the direct subtemplates are mapped first before the recursive subtemplates are visited.
+ For 'self' there is no recursion, i.e. the potential recursion can be implemented in ``map_fn`` itself.
+
+ This helper function is useful for modification of pulse templates without having to worry about the internal
+ structure.
+
+ The pulse registry feature is not yet supported by the method. When you need it, please open an issue on github.
+
+ Args:
+ map_fn: The function to be applied to the subtemplates according to the recursion strategy.
+ identifier_map: This function is called to map the identifiers of pulse templates that need to be newly created because their subtemplates changed.
+ always_new_template: If True, a new pulse template will be created even if the mapped subtemplates are identical.
+ recursion_strategy: Either 'pre', 'post' or 'self'.
+ - 'pre': All recursive subtemplates are mapped before the map_fn is applied.
+ - 'post': All recursive subtemplates are mapped after the map_fn is applied.
+ - 'Self': The recursion needs to be done by the map_fn if required.
+
+ Returns:
+ A pulse template with mapped subtemplates.
+ """
+ assert recursion_strategy in ('pre', 'post', 'self')
+
+ def map_templates_in_tree(tree):
+ if isinstance(tree, PulseTemplate):
+ pt = tree
+ if recursion_strategy == 'pre':
+ pt = pt.with_mapped_subtemplates(map_fn,
+ identifier_map=identifier_map,
+ always_new_template=always_new_template,
+ recursion_strategy=recursion_strategy,
+ )
+ pt = map_fn(pt)
+ if recursion_strategy == 'post':
+ pt = pt.with_mapped_subtemplates(map_fn,
+ identifier_map=identifier_map,
+ always_new_template=always_new_template,
+ recursion_strategy=recursion_strategy,
+ )
+ map_templates_in_tree.tree_changed |= pt is not tree
+ return pt
+
+ elif isinstance(tree, tuple):
+ return tuple(map(map_templates_in_tree, tree))
+ elif isinstance(tree, list):
+ return list(map(map_templates_in_tree, tree))
+ elif isinstance(tree, dict):
+ return {key: map_templates_in_tree(value)
+ for key, value in tree.items()}
+ else:
+ return tree
+ map_templates_in_tree.tree_changed = always_new_template
+
+ data = {name: value
+ for name, value in self.get_serialization_data().items()
+ if not name.startswith('#')}
+ mapped = map_templates_in_tree(data)
+ if map_templates_in_tree.tree_changed:
+ identifier = identifier_map(self.identifier)
+ return self.deserialize(**mapped, identifier=identifier)
+ else:
+ return self
+
+ def pad_to(self,
+ to_new_duration: Union[ExpressionLike, Callable[[Expression], ExpressionLike]],
+ spt_kwargs: Mapping[str, Any] = None,
+ pt_kwargs: Mapping[str, Any] = None,
+ ) -> 'PulseTemplate':
+ """Pad this pulse template to the given duration.
+ The target duration can be numeric, symbolic or a callable that returns a new duration from the current
+ duration.
+
+ Examples:
+ # pad to a fixed duration
+ >>> padded_1 = my_pt.pad_to(1000)
+
+ # pad to a fixed sample coun
+ >>> padded_2 = my_pt.pad_to('sample_rate * 1000')
+
+ # pad to the next muliple of 16 samples with a symbolic sample rate
+ >>> padded_3 = my_pt.pad_to(to_next_multiple('sample_rate', 16))
+
+ # pad to the next muliple of 16 samples with a fixed sample rate of 1 GHz
+ >>> padded_4 = my_pt.pad_to(to_next_multiple(1, 16))
+ Args:
+ to_new_duration: Duration or callable that maps the current duration to the new duration
+ spt_kwargs: Keyword arguments for an optionally newly created sequence pulse template.
+ pt_kwargs: Deprecated! Similar to ``spt_kwargs`` but enforces sequence pt creation even if no padding required.
+
+ Returns:
+ A pulse template that has the duration given by ``to_new_duration``. It can be ``self`` if the duration is
+ already as required. It is never ``self`` if ``pt_kwargs`` is non-empty (deprecated feated).
+ """
+ if pt_kwargs:
+ warnings.warn('pt_kwargs is deprecated and will be removed in a post 1.0 release. '
+ 'Please use spt_kwargs instead.',
+ DeprecationWarning, stacklevel=2)
+ spt_kwargs = spt_kwargs or {}
+ pt_kwargs = pt_kwargs or {}
+
+ from qupulse.pulses import ConstantPT, SequencePT
+ current_duration = self.duration
+ if callable(to_new_duration):
+ new_duration = to_new_duration(current_duration)
+ else:
+ new_duration = ExpressionScalar(to_new_duration)
+ pad_duration = new_duration - current_duration
+
+ if not pt_kwargs and pad_duration == 0:
+ return self
+
+ pad_pt = ConstantPT(pad_duration, self.final_values)
+ if pt_kwargs or spt_kwargs:
+ return SequencePT(self, pad_pt, **pt_kwargs, **spt_kwargs)
+ else:
+ return self @ pad_pt
+
+ def pad_selected_subtemplates_to(self,
+ to_new_duration: Union[ExpressionLike, Callable[[Expression], ExpressionLike]],
+ selector: Callable[['PulseTemplate'], bool] = None,
+ spt_kwargs: Mapping[str, Any] = None,
+ identity_map: Callable[[Optional[str]], Optional[str]] = lambda x: x if x is None else f"{x}_padded",
+ ):
+ """Pad all subtemplates for which the selector returns true with the given padding strategy. If no selector is
+ specified, all atomic subtemplates are padded. Padding non-atomic pulse templates is generally non-sensical when the subtemplates are padded.
+
+ By default newly created `SequencePT`s have the metadata field `to_single_waveform` set to "always".
+ Overwrite `pt_kwargs` to supply other metadata arguments.
+
+ If you need more customization, you can use :py:meth:`.PulseTemplate.with_mapped_subtemplates`.
+
+ !!! Note: This method needs to create new pulse templates and removes all parent tempates identifiers
+
+ Args:
+ to_new_duration: Specity how to pad. See :func:`pad_to`.
+ selector: Select which subtemplates to pad.
+ spt_kwargs: Passed to newly created sequence pulse templates required for padding. By default ``{"metadata": {"to_single_waveform": "always"}}`` is passed to make them atomic.
+ identity_map: Provide a function to map the identifiers of composite templates whos subtemplates are padded.
+
+ Returns:
+ A new pulse template if any subtemplate needed to be padded.
+ """
+ if selector is None:
+ def selector(pt: PulseTemplate) -> bool:
+ return pt._is_atomic()
+
+ if spt_kwargs is None:
+ spt_kwargs = {"metadata": {"to_single_waveform": "always"}}
+
+ if selector(self):
+ return self.pad_to(to_new_duration, spt_kwargs)
+
+ def map_fn(pt: PulseTemplate) -> PulseTemplate:
+ if selector(pt):
+ mapped = pt.pad_to(to_new_duration, spt_kwargs)
+ return mapped
+ else:
+ return pt.pad_selected_subtemplates_to(to_new_duration, selector, spt_kwargs)
+
+ return self.with_mapped_subtemplates(map_fn,
+ identifier_map=identity_map,
+ recursion_strategy='self')
+
+
def __format__(self, format_spec: str):
if format_spec == '':
format_spec = self._DEFAULT_FORMAT_SPEC
@@ -379,6 +615,8 @@ def __repr__(self):
kwargs = ','.join('%s=%r' % (key, value)
for key, value in self.get_serialization_data().items()
if key.isidentifier() and value is not None)
+ if self.identifier:
+ kwargs = f"identifier={self.identifier!r}," + kwargs
return '{type_name}({kwargs})'.format(type_name=type_name, kwargs=kwargs)
def __add__(self, other: ExpressionLike):
@@ -409,11 +647,29 @@ def __truediv__(self, other):
from qupulse.pulses.arithmetic_pulse_template import try_operation
return try_operation(self, '/', other)
+ def _get_compare_key(self):
+ """Compare pulse templates without runtime meta data."""
+ data = self.get_serialization_data()
+ data.pop("metadata", None)
+ return data
+
def __hash__(self):
if self.__cached_hash_value is None:
- self.__cached_hash_value = forced_hash(self.get_serialization_data())
+ self.__cached_hash_value = forced_hash(self._get_compare_key())
return self.__cached_hash_value
+ def __eq__(self, other):
+ if hasattr(other, '_get_compare_key') and hasattr(other, 'metadata'):
+ equal = self._get_compare_key() == other._get_compare_key()
+ if equal and self.metadata != other.metadata:
+ warnings.warn("PulseTemplate differ only in their metadata and are therefore regarded as equal. "
+ "This is required because the metadata field is mutable and the hash implementation requires consistent equality checks.",
+ stacklevel=2,
+ category=MetadataComparison)
+ return equal
+ else:
+ return NotImplemented
+
class AtomicPulseTemplate(PulseTemplate, MeasurementDefiner):
"""A PulseTemplate that does not imply any control flow disruptions and can be directly
@@ -425,8 +681,9 @@ class AtomicPulseTemplate(PulseTemplate, MeasurementDefiner):
def __init__(self, *,
identifier: Optional[str],
- measurements: Optional[List[MeasurementDeclaration]]):
- PulseTemplate.__init__(self, identifier=identifier)
+ measurements: Optional[List[MeasurementDeclaration]],
+ metadata: TemplateMetadata | dict = None):
+ PulseTemplate.__init__(self, identifier=identifier, metadata=metadata)
MeasurementDefiner.__init__(self, measurements=measurements)
def with_parallel_atomic(self, *parallel: 'AtomicPulseTemplate') -> 'AtomicPulseTemplate':
@@ -436,40 +693,29 @@ def with_parallel_atomic(self, *parallel: 'AtomicPulseTemplate') -> 'AtomicPulse
else:
return self
- @property
- def atomicity(self) -> bool:
- warnings.warn("Deprecated since neither maintained nor properly designed.", category=DeprecationWarning)
- return True
-
def _is_atomic(self) -> bool:
return True
measurement_names = MeasurementDefiner.measurement_names
- def _internal_create_program(self, *,
- scope: Scope,
- measurement_mapping: Dict[str, Optional[str]],
- channel_mapping: Dict[ChannelID, Optional[ChannelID]],
- global_transformation: Optional[Transformation],
- to_single_waveform: Set[Union[str, 'PulseTemplate']],
- parent_loop: Loop) -> None:
- """Parameter constraints are validated in build_waveform because build_waveform is guaranteed to be called
- during sequencing"""
- ### current behavior (same as previously): only adds EXEC Loop and measurements if a waveform exists.
- ### measurements are directly added to parent_loop (to reflect behavior of Sequencer + MultiChannelProgram)
+ def _internal_build_program(self, program_builder: ProgramBuilder):
+ context = program_builder.build_context
+ scope = context.scope
assert not scope.get_volatile_parameters().keys() & self.parameter_names, "AtomicPT cannot be volatile"
waveform = self.build_waveform(parameters=scope,
- channel_mapping=channel_mapping)
- if waveform:
- measurements = self.get_measurement_windows(parameters=scope,
- measurement_mapping=measurement_mapping)
-
- if global_transformation:
- waveform = TransformingWaveform.from_transformation(waveform, global_transformation)
-
- parent_loop.add_measurements(measurements=measurements)
- parent_loop.append_child(waveform=waveform)
+ channel_mapping=context.channel_mapping)
+ if not waveform:
+ return
+
+ measurements = self.get_measurement_windows(parameters=scope,
+ measurement_mapping=context.measurement_mapping)
+ program_builder.measure(measurements)
+ constant_values = waveform.constant_value_dict()
+ if constant_values is None:
+ program_builder.play_arbitrary_waveform(waveform)
+ else:
+ program_builder.hold_voltage(waveform.duration, constant_values)
@abstractmethod
def build_waveform(self,
@@ -531,3 +777,10 @@ def __str__(self) -> str:
self.templateA, self.templateB, ', '.join(self.names)
)
+
+class UnknownVolatileParameter(RuntimeWarning):
+ pass
+
+
+class MetadataComparison(RuntimeWarning):
+ pass
diff --git a/qupulse/pulses/pulse_template_parameter_mapping.py b/qupulse/pulses/pulse_template_parameter_mapping.py
deleted file mode 100644
index 5daf2695a..000000000
--- a/qupulse/pulses/pulse_template_parameter_mapping.py
+++ /dev/null
@@ -1,14 +0,0 @@
-"""..deprecated:: 0.1
-"""
-
-from qupulse.pulses.mapping_pulse_template import MappingPulseTemplate
-
-__all__ = ["MappingPulseTemplate"]
-
-import warnings
-warnings.warn("MappingPulseTemplate was moved from qupulse.pulses.pulse_template_parameter_mapping to "
- "qupulse.pulses.mapping_pulse_template. Please consider fixing your stored pulse templates by loading "
- "and storing them anew.", DeprecationWarning)
-
-from qupulse.serialization import SerializableMeta
-SerializableMeta.deserialization_callbacks["qupulse.pulses.pulse_template_parameter_mapping.MappingPulseTemplate"] = SerializableMeta.deserialization_callbacks[MappingPulseTemplate.get_type_identifier()]
diff --git a/qupulse/pulses/range.py b/qupulse/pulses/range.py
index 34f7e8a8e..950d15f0a 100644
--- a/qupulse/pulses/range.py
+++ b/qupulse/pulses/range.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
from typing import Tuple, Any, AbstractSet, Mapping, Union, Iterator
from numbers import Number
from dataclasses import dataclass
diff --git a/qupulse/pulses/repetition_pulse_template.py b/qupulse/pulses/repetition_pulse_template.py
index 809a91ce0..fd4f0eca8 100644
--- a/qupulse/pulses/repetition_pulse_template.py
+++ b/qupulse/pulses/repetition_pulse_template.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
"""This module defines RepetitionPulseTemplate, a higher-order hierarchical pulse template that
represents the n-times repetition of another PulseTemplate."""
@@ -8,13 +12,15 @@
import numpy as np
from qupulse.serialization import Serializer, PulseRegistryType
-from qupulse._program._loop import Loop, VolatileRepetitionCount
+from qupulse.program.volatile import VolatileRepetitionCount
+from qupulse.program import ProgramBuilder
from qupulse.parameter_scope import Scope
from qupulse.utils.types import ChannelID
from qupulse.expressions import ExpressionScalar
from qupulse.utils import checked_int_cast
from qupulse.pulses.pulse_template import PulseTemplate
+from qupulse.pulses.metadata import TemplateMetadata
from qupulse.pulses.loop_pulse_template import LoopPulseTemplate
from qupulse.pulses.parameters import ParameterConstrainer
from qupulse.pulses.measurement import MeasurementDefiner, MeasurementDeclaration
@@ -39,22 +45,35 @@ def __init__(self,
*args,
parameter_constraints: Optional[List]=None,
measurements: Optional[List[MeasurementDeclaration]]=None,
+ metadata: Union[TemplateMetadata, dict] = None,
registry: PulseRegistryType=None
) -> None:
- """Create a new RepetitionPulseTemplate instance.
+ """
+ Furthermore, this class allows associating an identifier, measurements, and parameter constraints with this sequence.
+ If the body evaluates to nothing during instantiation, the associated measurements are dropped.
+
+ Translation into a single waveform can be forced by passing ``to_single_waveform == 'always'`` in the ``metadata``.
+
+ The default creation does not flatten multiple nested repetition pulse templates.
+ Use :py:meth:`.RepetitionPulseTemplate.with_repetition` which will do that if ``identifier`` and ```metadata`` are not set.
+
+ Raises:
+ ValueError: If the repetition count is negative
Args:
- body (PulseTemplate): The PulseTemplate which will be repeated.
- repetition_count (int or ParameterDeclaration): The number of repetitions either as a
- constant integer value or as a parameter declaration.
- identifier (str): A unique identifier for use in serialization. (optional)
+ body: The PulseTemplate which will be repeated.
+ repetition_count: The number of repetitions.
+ identifier: A unique identifier for use in serialization.
+ parameter_constraints: See :py:class:`.ParameterConstrainer` for details
+ metadata: Used to initialize :py:attr:`.PulseTemplate.metadata`
+ registry: This pulse template registers itself there under the given identifier if supplied.
"""
if len(args) == 1 and parameter_constraints is None:
warn('You used parameter_constraints as a positional argument. It will be keyword only in a future version.', DeprecationWarning)
elif args:
TypeError('RepetitionPulseTemplate expects 3 positional arguments, got ' + str(3 + len(args)))
- LoopPulseTemplate.__init__(self, identifier=identifier, body=body)
+ LoopPulseTemplate.__init__(self, identifier=identifier, body=body, metadata=metadata)
ParameterConstrainer.__init__(self, parameter_constraints=parameter_constraints)
MeasurementDefiner.__init__(self, measurements=measurements)
@@ -70,8 +89,9 @@ def __init__(self,
self._register(registry=registry)
- def with_repetition(self, repetition_count: Union[int, str, ExpressionScalar]) -> 'PulseTemplate':
- if self.identifier:
+ def with_repetition(self, repetition_count: Union[int, str, ExpressionScalar]) -> 'RepetitionPulseTemplate':
+ repetition_count = ExpressionScalar.make(repetition_count)
+ if self.identifier or self.metadata:
return RepetitionPulseTemplate(self, repetition_count)
else:
return RepetitionPulseTemplate(
@@ -112,39 +132,23 @@ def measurement_names(self) -> AbstractSet[str]:
def duration(self) -> ExpressionScalar:
return self.repetition_count * self.body.duration
- def _internal_create_program(self, *,
- scope: Scope,
- measurement_mapping: Dict[str, Optional[str]],
- channel_mapping: Dict[ChannelID, Optional[ChannelID]],
- global_transformation: Optional['Transformation'],
- to_single_waveform: AbstractSet[Union[str, 'PulseTemplate']],
- parent_loop: Loop) -> None:
- self.validate_scope(scope)
-
- repetition_count = max(0, self.get_repetition_count_value(scope))
-
- # todo (2018-07-19): could in some circumstances possibly just multiply subprogram repetition count?
- # could be tricky if any repetition count is volatile ? check later and optimize if necessary
- if repetition_count > 0:
- if scope.get_volatile_parameters().keys() & self.repetition_count.variables:
- repetition_definition = VolatileRepetitionCount(self.repetition_count, scope)
- assert int(repetition_definition) == repetition_count
- else:
- repetition_definition = repetition_count
-
- repj_loop = Loop(repetition_count=repetition_definition)
- self.body._create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- global_transformation=global_transformation,
- to_single_waveform=to_single_waveform,
- parent_loop=repj_loop)
- if repj_loop.waveform is not None or len(repj_loop.children) > 0:
- measurements = self.get_measurement_windows(scope, measurement_mapping)
- if measurements:
- parent_loop.add_measurements(measurements)
-
- parent_loop.append_child(loop=repj_loop)
+ def _internal_build_program(self, program_builder: ProgramBuilder):
+ build_context = program_builder.build_context
+ scope = build_context.scope
+
+ repetition_count = self.get_repetition_count_value(scope)
+ if not (repetition_count > 0):
+ return
+
+ if scope.get_volatile_parameters().keys() & self.repetition_count.variables:
+ repetition_definition = VolatileRepetitionCount(self.repetition_count, scope)
+ assert int(repetition_definition) == repetition_count
+ else:
+ repetition_definition = repetition_count
+
+ measurements = self.get_measurement_windows(scope, build_context.measurement_mapping)
+ for repetition_program_builder in program_builder.with_repetition(repetition_definition, measurements=measurements):
+ self.body._build_program(program_builder=repetition_program_builder)
def get_serialization_data(self, serializer: Optional[Serializer]=None) -> Dict[str, Any]:
data = super().get_serialization_data(serializer)
diff --git a/qupulse/pulses/sequence_pulse_template.py b/qupulse/pulses/sequence_pulse_template.py
index 1ef1bed0b..c1fe64ba9 100644
--- a/qupulse/pulses/sequence_pulse_template.py
+++ b/qupulse/pulses/sequence_pulse_template.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
"""This module defines SequencePulseTemplate, a higher-order hierarchical pulse template that
combines several other PulseTemplate objects for sequential execution."""
@@ -8,14 +12,15 @@
import warnings
from qupulse.serialization import Serializer, PulseRegistryType
-from qupulse._program._loop import Loop
+from qupulse.program import ProgramBuilder
from qupulse.parameter_scope import Scope
from qupulse.utils import cached_property
from qupulse.utils.types import MeasurementWindow, ChannelID, TimeType
from qupulse.pulses.pulse_template import PulseTemplate, AtomicPulseTemplate
+from qupulse.pulses.metadata import TemplateMetadata
from qupulse.pulses.parameters import ConstraintLike, ParameterConstrainer
from qupulse.pulses.mapping_pulse_template import MappingPulseTemplate, MappingTuple
-from qupulse._program.waveforms import SequenceWaveform
+from qupulse.program.waveforms import SequenceWaveform
from qupulse.pulses.measurement import MeasurementDeclaration, MeasurementDefiner
from qupulse.expressions import Expression, ExpressionScalar
@@ -25,14 +30,15 @@
class SequencePulseTemplate(PulseTemplate, ParameterConstrainer, MeasurementDefiner):
"""A sequence of different PulseTemplates.
- SequencePulseTemplate allows to group several
- PulseTemplates (subtemplates) into one larger sequence,
- i.e., when instantiating a pulse from a SequencePulseTemplate
- all pulses instantiated from the subtemplates are queued for
- execution right after one another.
- SequencePulseTemplate requires to specify a mapping of
- parameter declarations from its subtemplates to its own, enabling
- renaming and mathematical transformation of parameters.
+ SequencePulseTemplate allows grouping several PulseTemplates (subtemplates) into one larger sequence.
+ When instantiating a pulse from a SequencePulseTemplate all pulses instantiated from the subtemplates are queued
+ right after one another.
+
+ Furthermore, this class allows associating an identifier, measurements, and parameter constraints with this sequence.
+ If none of the subtemplates evaluate to anything during instantiation, the associated measurements are dropped.
+
+ For more concise syntax, the subtemplate can be stated in the form of a "mapping tuple" that is passed to :py:func:`.MappingPulseTemplate.from_tuple`.
+ This allows the mathematical mapping if pulse parameters and renaming of channels and measurement declarations.
"""
def __init__(self,
@@ -40,27 +46,27 @@ def __init__(self,
identifier: Optional[str]=None,
parameter_constraints: Optional[Iterable[ConstraintLike]]=None,
measurements: Optional[List[MeasurementDeclaration]]=None,
+ metadata: Union[TemplateMetadata, dict] = None,
registry: PulseRegistryType=None) -> None:
- """Create a new SequencePulseTemplate instance.
+ """
+ The only required arguments are the subtemplates. Besides creating :py:class:`.MappingPulseTemplate` s from tuples,
+ the subtemplates are not modified and particularly nested sequences are not flattened in hierarchy.
+ Use :py:meth:`.SequencePulseTemplate.concatenate` or the ``@`` operator if you want automatic flattening.
- Requires a (correctly ordered) list of subtemplates in the form
- (PulseTemplate, Dict(str -> str)) where the dictionary is a mapping between the external
- parameters exposed by this SequencePulseTemplate to the parameters declared by the
- subtemplates, specifying how the latter are derived from the former, i.e., the mapping is
- subtemplate_parameter_name -> mapping_expression (as str) where the free variables in the
- mapping_expression are parameters declared by this SequencePulseTemplate.
+ You can specify ``to_single_waveform == 'always'`` in the metadata to enforce translation into a single waveform.
- The following requirements must be satisfied:
- - for each parameter declared by a subtemplate, a mapping expression must be provided
- - each free variable in a mapping expression must be declared as an external parameter
- of this SequencePulseTemplate
+ Raises:
+ ValueError: if the subtemplates are defined on different channels.
Args:
- subtemplates (List(Subtemplate)): The list of subtemplates of this
- SequencePulseTemplate as tuples of the form (PulseTemplate, Dict(str -> str)).
- identifier (str): A unique identifier for use in serialization. (optional)
+ subtemplates: The subtemplates given as `PulseTemplate` or as a tuple compatible with :py:meth:`.MappingPulseTemplate.from_tuple`.
+ identifier: A unique identifier for use in serialization.
+ parameter_constraints: A list of constraints checked on instantiation.
+ measurements: A list of measurement declarations associated with this sequence.
+ metadata: An optional metadata associated with this sequence.
+ registry: A PulseRegistryType or a subclass of PulseRegistryType.
"""
- PulseTemplate.__init__(self, identifier=identifier)
+ PulseTemplate.__init__(self, identifier=identifier, metadata=metadata)
ParameterConstrainer.__init__(self, parameter_constraints=parameter_constraints)
MeasurementDefiner.__init__(self, measurements=measurements)
@@ -92,6 +98,7 @@ def concatenate(cls, *pulse_templates: Union[PulseTemplate, MappingTuple], **kwa
if (isinstance(pt, SequencePulseTemplate)
and pt.identifier is None
and not pt.measurement_declarations
+ and not pt.metadata
and not pt.parameter_constraints):
parsed.extend(pt.subtemplates)
else:
@@ -127,27 +134,12 @@ def build_waveform(self,
[sub_template.build_waveform(parameters, channel_mapping=channel_mapping)
for sub_template in self.__subtemplates])
- def _internal_create_program(self, *,
- scope: Scope,
- measurement_mapping: Dict[str, Optional[str]],
- channel_mapping: Dict[ChannelID, Optional[ChannelID]],
- global_transformation: Optional['Transformation'],
- to_single_waveform: Set[Union[str, 'PulseTemplate']],
- parent_loop: Loop) -> None:
- self.validate_scope(scope)
-
- if self.duration.evaluate_in_scope(scope) > 0:
- measurements = self.get_measurement_windows(scope, measurement_mapping)
- if measurements:
- parent_loop.add_measurements(measurements)
-
+ def _internal_build_program(self, program_builder: ProgramBuilder):
+ build_context = program_builder.build_context
+ measurements = self.get_measurement_windows(build_context.scope, build_context.measurement_mapping)
+ with program_builder.with_sequence(measurements=measurements) as sequence_program_builder:
for subtemplate in self.subtemplates:
- subtemplate._create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- global_transformation=global_transformation,
- to_single_waveform=to_single_waveform,
- parent_loop=parent_loop)
+ subtemplate._build_program(program_builder=sequence_program_builder)
def get_serialization_data(self, serializer: Optional[Serializer]=None) -> Dict[str, Any]:
data = super().get_serialization_data(serializer)
diff --git a/qupulse/pulses/table_pulse_template.py b/qupulse/pulses/table_pulse_template.py
index 873bbdf93..785cad72b 100644
--- a/qupulse/pulses/table_pulse_template.py
+++ b/qupulse/pulses/table_pulse_template.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
"""This module defines the TablePulseTemplate, one of the elementary pulse templates and its
waveform representation.
@@ -16,6 +20,7 @@
import sympy
from sympy.logic.boolalg import BooleanAtom
+from qupulse.pulses.metadata import TemplateMetadata
from qupulse.utils import pairwise
from qupulse.utils.types import ChannelID
from qupulse.serialization import Serializer, PulseRegistryType
@@ -23,7 +28,7 @@
from qupulse.pulses.pulse_template import AtomicPulseTemplate, MeasurementDeclaration
from qupulse.pulses.interpolation import InterpolationStrategy, LinearInterpolationStrategy, \
HoldInterpolationStrategy, JumpInterpolationStrategy
-from qupulse._program.waveforms import TableWaveform, TableWaveformEntry
+from qupulse.program.waveforms import TableWaveform, TableWaveformEntry
from qupulse.expressions import ExpressionScalar, Expression
from qupulse.pulses.multi_channel_pulse_template import MultiChannelWaveform
@@ -149,7 +154,9 @@ def __init__(self, entries: Dict[ChannelID, Sequence[EntryInInit]],
parameter_constraints: Optional[List[Union[str, ParameterConstraint]]]=None,
measurements: Optional[List[MeasurementDeclaration]]=None,
consistency_check: bool=True,
- registry: PulseRegistryType=None) -> None:
+ registry: PulseRegistryType=None,
+ metadata: TemplateMetadata | dict = None,
+ ) -> None:
"""
Construct a `TablePulseTemplate` from a dict which maps channels to their entries. By default the consistency
of the provided entries is checked. There are two static functions for convenience construction: from_array and
@@ -163,7 +170,7 @@ def __init__(self, entries: Dict[ChannelID, Sequence[EntryInInit]],
measurements: Measurement declaration list that is forwarded to the MeasurementDefiner superclass
consistency_check: If True the consistency of the times will be checked on construction as far as possible
"""
- AtomicPulseTemplate.__init__(self, identifier=identifier, measurements=measurements)
+ AtomicPulseTemplate.__init__(self, identifier=identifier, measurements=measurements, metadata=metadata)
ParameterConstrainer.__init__(self, parameter_constraints=parameter_constraints)
if not entries:
@@ -259,6 +266,9 @@ def get_entries_instantiated(self, parameters: Dict[str, numbers.Real]) \
duration = max(instantiated[-1].t for instantiated in instantiated_entries.values())
+ if duration == 0:
+ return {}
+
# ensure that all channels have equal duration
for channel, instantiated in instantiated_entries.items():
final_entry = instantiated[-1]
@@ -321,9 +331,6 @@ def build_waveform(self,
if not instantiated:
return None
- if self.duration.evaluate_numeric(**parameters) == 0:
- return None
-
waveforms = [TableWaveform.from_table(*ch_instantiated)
for ch_instantiated in instantiated]
diff --git a/qupulse/pulses/time_reversal_pulse_template.py b/qupulse/pulses/time_reversal_pulse_template.py
index a4758d1a7..ebaca7f27 100644
--- a/qupulse/pulses/time_reversal_pulse_template.py
+++ b/qupulse/pulses/time_reversal_pulse_template.py
@@ -1,8 +1,13 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
from typing import Optional, Set, Dict, Union
from qupulse import ChannelID
-from qupulse._program._loop import Loop
-from qupulse._program.waveforms import Waveform
+from qupulse.program import ProgramBuilder
+from qupulse.program.waveforms import Waveform
+from qupulse.pulses.metadata import TemplateMetadata
from qupulse.serialization import PulseRegistryType
from qupulse.expressions import ExpressionScalar
@@ -14,8 +19,10 @@ class TimeReversalPulseTemplate(PulseTemplate):
def __init__(self, inner: PulseTemplate,
identifier: Optional[str] = None,
- registry: PulseRegistryType = None):
- super(TimeReversalPulseTemplate, self).__init__(identifier=identifier)
+ registry: PulseRegistryType = None,
+ metadata: TemplateMetadata | dict = None,
+ ):
+ super(TimeReversalPulseTemplate, self).__init__(identifier=identifier, metadata=metadata)
self._inner = inner
self._register(registry=registry)
@@ -45,13 +52,18 @@ def defined_channels(self) -> Set['ChannelID']:
@property
def integral(self) -> Dict[ChannelID, ExpressionScalar]:
return self._inner.integral
+
+ @property
+ def initial_values(self) -> Dict[ChannelID, ExpressionScalar]:
+ return self._inner.final_values
- def _internal_create_program(self, *, parent_loop: Loop, **kwargs) -> None:
- inner_loop = Loop()
- self._inner._internal_create_program(parent_loop=inner_loop, **kwargs)
- inner_loop.reverse_inplace()
+ @property
+ def final_values(self) -> Dict[ChannelID, ExpressionScalar]:
+ return self._inner.initial_values
- parent_loop.append_child(inner_loop)
+ def _internal_build_program(self, program_builder: ProgramBuilder):
+ with program_builder.time_reversed() as inner_program_builder:
+ self._inner._internal_build_program(inner_program_builder)
def build_waveform(self,
*args, **kwargs) -> Optional[Waveform]:
diff --git a/qupulse/serialization.py b/qupulse/serialization.py
index 3825057ed..f0b80f6a1 100644
--- a/qupulse/serialization.py
+++ b/qupulse/serialization.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
""" This module provides serialization and storage functionality.
Classes:
@@ -766,7 +770,20 @@ def deserialize(self, representation: Union[str, Dict[str, Any]]) -> Serializabl
else:
repr_ = dict(representation)
- module_name, class_name = repr_['type'].rsplit('.', 1)
+ package_name, *module_path, class_name = repr_['type'].split('.')
+
+ # the qctoolkit alias was removed. We hack in the new name here directly
+ if package_name == 'qctoolkit':
+ package_name = 'qupulse'
+
+ # this alias was removed so we hack in the "new" location
+ if module_path and module_path[-1] == 'pulse_template_parameter_mapping':
+ module_path[-1] = 'mapping_pulse_template'
+
+ module_path.insert(0, package_name)
+
+ module_name = '.'.join(module_path)
+
module = __import__(module_name, fromlist=[class_name])
class_ = getattr(module, class_name)
@@ -1024,7 +1041,11 @@ def filter_serializables(self, obj_dict) -> Any:
if get_default_pulse_registry() is self.storage:
registry = dict()
- return deserialization_callback(identifier=obj_identifier, registry=registry, **obj_dict)
+ try:
+ return deserialization_callback(identifier=obj_identifier, registry=registry, **obj_dict)
+ except Exception as err:
+ raise ValueError(f"Unable to deserialize {type_identifier} from {obj_dict}",
+ type_identifier, obj_dict) from err
return obj_dict
diff --git a/qupulse/utils/__init__.py b/qupulse/utils/__init__.py
index 43de28c1c..fa5715379 100644
--- a/qupulse/utils/__init__.py
+++ b/qupulse/utils/__init__.py
@@ -1,13 +1,19 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
"""This package contains utility functions and classes as well as custom sympy extensions(hacks)."""
-from typing import Union, Iterable, Any, Tuple, Mapping, Iterator, TypeVar, Sequence, AbstractSet
+from typing import Union, Iterable, Any, Tuple, Mapping, Iterator, TypeVar, Sequence, AbstractSet, Optional, Callable
import itertools
import re
import numbers
from collections import OrderedDict
from frozendict import frozendict
+from qupulse.expressions import ExpressionScalar, ExpressionLike
import numpy
+import sympy as sp
try:
from math import isclose
@@ -25,7 +31,7 @@
__all__ = ["checked_int_cast", "is_integer", "isclose", "pairwise", "replace_multiple", "cached_property",
- "forced_hash"]
+ "forced_hash", "to_next_multiple"]
def checked_int_cast(x: Union[float, int, numpy.ndarray], epsilon: float=1e-6) -> int:
@@ -122,3 +128,47 @@ def forced_hash(obj) -> int:
return hash(tuple(map(forced_hash, obj)))
raise
+
+
+def to_next_multiple(sample_rate: ExpressionLike, quantum: int,
+ min_quanta: Optional[int] = None) -> Callable[[ExpressionScalar], ExpressionLike]:
+ """Construct a helper function to expand a duration to one corresponding to
+ valid sample multiples according to the arguments given.
+ Useful e.g. for PulseTemplate.pad_to's 'to_new_duration'-argument.
+
+ Args:
+ sample_rate: sample rate with respect to which the duration is evaluated.
+ quantum: number of samples to whose next integer multiple the duration shall be rounded up to.
+ min_quanta: number of multiples of quantum not to fall short of.
+ Returns:
+ A function that takes a duration as input, and returns
+ a duration rounded up to the next valid samples count in given sample rate.
+ The function returns 0 if duration==0, <0 is not checked if min_quanta is None.
+
+ """
+ sample_rate = ExpressionScalar(sample_rate)
+
+ #is it more efficient to omit the Max call if not necessary?
+ if min_quanta is None:
+ #double negative for ceil division.
+ return lambda duration: -(-(duration*sample_rate)//quantum) * (quantum/sample_rate)
+ else:
+ # work with sympy
+ sample_rate = sample_rate.sympified_expression
+ duration_per_quantum = sp.Integer(quantum) / sample_rate
+ minimal_duration = duration_per_quantum * min_quanta
+
+ def build_next_multiple(duration: ExpressionScalar) -> ExpressionLike:
+ duration = sp.sympify(duration)
+ n_quanta = sp.ceiling(duration / duration_per_quantum)
+ rounded_up_duration = n_quanta * duration_per_quantum
+
+ next_multiple_sp = sp.Piecewise(
+ (0, sp.Le(n_quanta, 0)),
+ (minimal_duration, sp.Le(n_quanta, min_quanta)),
+ (rounded_up_duration, True),
+ evaluate=False,
+ )
+ return ExpressionScalar(next_multiple_sp)
+
+ return build_next_multiple
diff --git a/qupulse/utils/numeric.py b/qupulse/utils/numeric.py
index b7b505f9d..fc0f9e3a5 100644
--- a/qupulse/utils/numeric.py
+++ b/qupulse/utils/numeric.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
from typing import Tuple, Type
from numbers import Rational
from math import gcd
@@ -7,9 +11,16 @@
import sympy
-def lcm(a: int, b: int):
- """least common multiple"""
- return a * b // gcd(a, b)
+try:
+ from math import lcm
+except ImportError:
+ # python version < 3.9
+ def lcm(*integers: int):
+ """Re-implementation of the least common multiple function that is in the standard library since python 3.9"""
+ result = 1
+ for value in integers:
+ result = result * value // gcd(value, result)
+ return result
def smallest_factor_ge(n: int, min_factor: int, brute_force: int = 5):
diff --git a/qupulse/utils/performance.py b/qupulse/utils/performance.py
index 4076b664c..0219f83f9 100644
--- a/qupulse/utils/performance.py
+++ b/qupulse/utils/performance.py
@@ -1,3 +1,8 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
+import warnings
from typing import Tuple
import numpy as np
@@ -24,6 +29,76 @@ def _is_monotonic_numpy(arr: np.ndarray) -> bool:
return np.all(arr[1:] >= arr[:-1])
+def _shrink_overlapping_windows_numpy(begins, lengths) -> bool:
+ supported_dtypes = ('int64', 'uint64')
+ if begins.dtype.name not in supported_dtypes or lengths.dtype.name not in supported_dtypes:
+ raise NotImplementedError("This function only supports 64 bit integer types yet.")
+
+ ends = begins + lengths
+
+ overlaps = np.zeros_like(ends, dtype=np.int64)
+ np.maximum(ends[:-1].view(np.int64) - begins[1:].view(np.int64), 0, out=overlaps[1:])
+
+ if np.any(overlaps >= lengths):
+ raise ValueError("Overlap is bigger than measurement window")
+ if np.any(overlaps > 0):
+ begins += overlaps.view(begins.dtype)
+ lengths -= overlaps.view(lengths.dtype)
+ return True
+ return False
+
+
+@njit
+def _shrink_overlapping_windows_numba(begins, lengths) -> bool:
+ shrank = False
+ for idx in range(len(begins) - 1):
+ end = begins[idx] + lengths[idx]
+ next_begin = begins[idx + 1]
+
+ if end > next_begin:
+ overlap = end - next_begin
+ shrank = True
+ if lengths[idx + 1] > overlap:
+ begins[idx + 1] += overlap
+ lengths[idx + 1] -= overlap
+ else:
+ raise ValueError("Overlap is bigger than measurement window")
+ return shrank
+
+
+class WindowOverlapWarning(RuntimeWarning):
+ COMMENT = (" This warning is an error by default. "
+ "Call 'warnings.simplefilter(WindowOverlapWarning, \"always\")' "
+ "to demote it to a regular warning.")
+
+ def __str__(self):
+ return super().__str__() + self.COMMENT
+
+
+warnings.simplefilter(category=WindowOverlapWarning, action='error')
+
+
+def shrink_overlapping_windows(begins, lengths, use_numba: bool = numba is not None) -> Tuple[np.array, np.array]:
+ """Shrink windows in place if they overlap. Emits WindowOverlapWarning if a window was shrunk.
+
+ Raises:
+ ValueError: if the overlap is bigger than a window.
+
+ Warnings:
+ WindowOverlapWarning
+ """
+ if use_numba:
+ backend = _shrink_overlapping_windows_numba
+ else:
+ backend = _shrink_overlapping_windows_numpy
+ begins = begins.copy()
+ lengths = lengths.copy()
+ if backend(begins, lengths):
+ warnings.warn("Found overlapping measurement windows which can be automatically shrunken if possible.",
+ category=WindowOverlapWarning)
+ return begins, lengths
+
+
@njit
def _time_windows_to_samples_sorted_numba(begins, lengths,
sample_rate: float) -> Tuple[np.ndarray, np.ndarray]:
@@ -81,4 +156,94 @@ def time_windows_to_samples(begins: np.ndarray, lengths: np.ndarray,
is_monotonic = _is_monotonic_numba
+@njit
+def _average_windows_numba(time: np.ndarray, values: np.ndarray,
+ begins: np.ndarray, ends: np.ndarray) -> np.ndarray:
+ n_samples, = time.shape
+ n_windows, = begins.shape
+
+ assert len(begins) == len(ends)
+ assert values.shape[0] == n_samples
+
+ result = np.zeros(begins.shape + values.shape[1:], dtype=float)
+ count = np.zeros(n_windows, dtype=np.uint64)
+
+ start = 0
+ for i in range(n_samples):
+ t = time[i]
+ v = values[i, ...]
+
+ while start < n_windows and ends[start] <= t:
+ n = count[start]
+ if n == 0:
+ result[start] = np.nan
+ else:
+ result[start] /= n
+ start += 1
+
+ idx = start
+ while idx < n_windows and begins[idx] <= t:
+ result[idx] += v
+ count[idx] += 1
+ idx += 1
+
+ for idx in range(start, n_windows):
+ n = count[idx]
+ if n == 0:
+ result[idx] = np.nan
+ else:
+ result[idx] /= count[idx]
+
+ return result
+
+
+def _average_windows_numpy(time: np.ndarray, values: np.ndarray,
+ begins: np.ndarray, ends: np.ndarray) -> np.ndarray:
+ start = np.searchsorted(time, begins)
+ end = np.searchsorted(time, ends)
+
+ val_shape = values.shape[1:]
+
+ count = end - start
+ val_mask = result_mask = start < end
+
+ result = np.zeros(begins.shape + val_shape, dtype=float)
+ while np.any(val_mask):
+ result[val_mask, ...] += values[start[val_mask], ...]
+ start[val_mask] += 1
+ val_mask = start < end
+
+ result[~result_mask, ...] = np.nan
+ if result.ndim == 1:
+ result[result_mask, ...] /= count[result_mask]
+ else:
+ result[result_mask, ...] /= count[result_mask, None]
+
+ return result
+
+def average_windows(time: np.ndarray, values: np.ndarray, begins: np.ndarray, ends: np.ndarray):
+ """This function calculates the average over all windows that are defined by begins and ends.
+ The function assumes that the given time array is monotonically increasing and might produce
+ nonsensical results if not.
+
+ Args:
+ time: Time associated with the values of shape (n_samples,)
+ values: Values to average of shape (n_samples,) or (n_samples, n_channels)
+ begins: Beginning time stamps of the windows of shape (n_windows,)
+ ends: Ending time stamps of the windows of shape (n_windows,)
+
+ Returns:
+ Averaged values for each window of shape (n_windows,) or (n_windows, n_channels).
+ Windows without samples are NaN.
+ """
+ n_samples, = time.shape
+ n_windows, = begins.shape
+
+ assert n_windows == len(ends)
+ assert values.shape[0] == n_samples
+
+ if numba is None:
+ return _average_windows_numpy(time, values, begins, ends)
+ else:
+ return _average_windows_numba(time, values, begins, ends)
diff --git a/qupulse/utils/sympy.py b/qupulse/utils/sympy.py
index b4d7286f2..06c280365 100644
--- a/qupulse/utils/sympy.py
+++ b/qupulse/utils/sympy.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
from typing import Union, Dict, Tuple, Any, Sequence, Optional, Callable
from numbers import Number
from types import CodeType
@@ -11,22 +15,35 @@
import numpy
try:
- from sympy.printing.numpy import NumPyPrinter
+ import scipy
+except ImportError:
+ scipy = None
+
+try:
+ from sympy.printing.numpy import NumPyPrinter, SciPyPrinter
except ImportError:
# sympy moved NumPyPrinter in release 1.8
from sympy.printing.pycode import NumPyPrinter
+ SciPyPrinter = None
warnings.warn("Please update sympy.", DeprecationWarning)
-try:
+
+if scipy:
import scipy.special as _special_functions
-except ImportError:
+else:
_special_functions = {fname: numpy.vectorize(fobject)
for fname, fobject in math.__dict__.items()
- if not fname.startswith('_') and fname not in numpy.__dict__}
+ if callable(fobject) and not fname.startswith('_') and fname not in numpy.__dict__}
warnings.warn('scipy is not installed. This reduces the set of available functions to those present in numpy + '
'manually vectorized functions in math.')
+if scipy and SciPyPrinter:
+ PrinterBase = SciPyPrinter
+else:
+ PrinterBase = NumPyPrinter
+
+
__all__ = ["sympify", "substitute_with_eval", "to_numpy", "get_variables", "get_free_symbols", "recursive_substitution",
"evaluate_lambdified", "get_most_simple_representation"]
@@ -184,6 +201,9 @@ def eval(cls, arg) -> Optional[sympy.Integer]:
if hasattr(arg, '__len__'):
return sympy.Integer(len(arg))
+ def _lambdacode(self, printer) -> str:
+ return f'len({printer._print(self.args[0])})'
+
is_Integer = True
Len.__name__ = 'len'
@@ -282,15 +302,30 @@ def sympify(expr: Union[str, Number, sympy.Expr, numpy.str_], **kwargs) -> sympy
raise
+class _LosslessFloatPrinter(sympy.StrPrinter):
+ def _print_Float(self, f):
+ # Keep normal formatting unless it would break the round-trip
+ # for this we check using sympy.Float instead of sympy.sympify for performance reasons
+ normal_repr = super()._print_Float(f)
+ if sympy.Float(normal_repr) == f:
+ return normal_repr
+ else:
+ return sympy.srepr(f)
+
+
def get_most_simple_representation(expression: sympy.Expr) -> Union[str, int, float]:
- if expression.free_symbols:
- return str(expression)
- elif expression.is_Integer:
- return int(expression)
- elif expression.is_Float:
- return float(expression)
- else:
- return str(expression)
+ str_repr = _LosslessFloatPrinter().doprint(expression)
+
+ # try if we have valid python literals
+ try:
+ return int(str_repr)
+ except ValueError:
+ pass
+ try:
+ return float(str_repr)
+ except ValueError:
+ pass
+ return str_repr
def get_free_symbols(expression: sympy.Expr) -> Sequence[sympy.Symbol]:
@@ -363,7 +398,7 @@ def recursive_substitution(expression: sympy.Expr,
return _recursive_substitution(expression, substitutions)
-_base_environment = {'builtins': builtins, '__builtins__': builtins}
+_base_environment = {'builtins': builtins, '__builtins__': builtins, 'math': math}
_math_environment = {**_base_environment, **math.__dict__}
_numpy_environment = {**_base_environment, **numpy.__dict__}
_sympy_environment = {**_base_environment, **sympy.__dict__}
@@ -371,6 +406,10 @@ def recursive_substitution(expression: sympy.Expr,
_lambdify_modules = [{'ceiling': numpy_compatible_ceiling, 'floor': _floor_to_int,
'Broadcast': numpy.broadcast_to}, 'numpy', _special_functions]
+if scipy:
+ # this is required for Integral lambdification
+ _lambdify_modules.append("scipy")
+
def evaluate_compiled(expression: sympy.Expr,
parameters: Dict[str, Union[numpy.ndarray, Number]],
@@ -397,7 +436,7 @@ def evaluate_lambdified(expression: Union[sympy.Expr, numpy.ndarray],
return lambdified(**parameters), lambdified
-class HighPrecPrinter(NumPyPrinter):
+class HighPrecPrinter(PrinterBase):
"""Custom printer that translates sympy.Rational into TimeType"""
def _print_Rational(self, expr):
return f'TimeType.from_fraction({expr.p}, {expr.q})'
diff --git a/qupulse/utils/tree.py b/qupulse/utils/tree.py
index 2585a5f57..5592e4e2b 100644
--- a/qupulse/utils/tree.py
+++ b/qupulse/utils/tree.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
"""This module contains a tree implementation."""
from typing import Iterable, Union, List, Generator, Tuple, TypeVar, Optional, Sequence
diff --git a/qupulse/utils/types.py b/qupulse/utils/types.py
index 7ec9d8c7c..3fd25fb9c 100644
--- a/qupulse/utils/types.py
+++ b/qupulse/utils/types.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
+#
+# SPDX-License-Identifier: LGPL-3.0-or-later
+
import typing
import abc
import inspect
@@ -7,35 +11,21 @@
import warnings
import collections
import operator
+import difflib
import numpy
import sympy
-
-try:
- from frozendict import frozendict
-except ImportError:
- warnings.warn("The frozendict package is not installed. We currently also ship a fallback frozendict which "
- "will be removed in a future release.", category=DeprecationWarning)
- frozendict = None
+import gmpy2
+from frozendict import frozendict
import qupulse.utils.numeric as qupulse_numeric
__all__ = ["MeasurementWindow", "ChannelID", "HashableNumpyArray", "TimeType", "time_from_float", "DocStringABCMeta",
- "SingletonABCMeta", "SequenceProxy", "frozendict"]
+ "SingletonABCMeta", "SequenceProxy"]
MeasurementWindow = typing.Tuple[str, numbers.Real, numbers.Real]
ChannelID = typing.Union[str, int]
-try:
- import gmpy2
- qupulse_numeric.FractionType = gmpy2.mpq
-
-except ImportError:
- gmpy2 = None
-
- warnings.warn('gmpy2 not found. Using fractions.Fraction as fallback. Install gmpy2 for better performance.'
- 'time_from_float might produce slightly different results')
-
def _with_other_as_time_type(fn):
"""This is decorator to convert the other argument and the result into a :class:`TimeType`"""
@@ -53,17 +43,16 @@ def wrapper(self, other) -> 'TimeType':
class TimeType:
- """This type represents a rational number with arbitrary precision.
-
- Internally it uses :func:`gmpy2.mpq` (if available) or :class:`fractions.Fraction`
- """
__slots__ = ('_value',)
- _InternalType = fractions.Fraction if gmpy2 is None else type(gmpy2.mpq())
- _to_internal = fractions.Fraction if gmpy2 is None else gmpy2.mpq
+ _InternalType = type(gmpy2.mpq())
+ _to_internal = gmpy2.mpq
def __init__(self, value: typing.Union[numbers.Rational, int] = 0., denominator: typing.Optional[int] = None):
- """
+ """This type represents a rational number with arbitrary precision.
+
+ Internally it uses :func:`gmpy2.mpq` which is considered an implementation detail.
+
Args:
value: interpreted as Rational if denominator is None. interpreted as numerator otherwise
denominator: Denominator of the Fraction if not None
@@ -300,6 +289,14 @@ def from_fraction(cls, numerator: int, denominator: int) -> 'TimeType':
"""
return cls(numerator, denominator)
+ @classmethod
+ def from_sympy(cls, num: sympy.Expr):
+ if num.is_Float:
+ return cls.from_float(float(num))
+ else:
+ p, q = num.as_numer_denom()
+ return cls.from_fraction(int(p), int(q))
+
def __repr__(self):
return f'TimeType({self._value.numerator}, {self._value.denominator})'
@@ -318,7 +315,7 @@ def __float__(self):
float: TimeType.from_float,
TimeType._InternalType: TimeType,
fractions.Fraction: TimeType,
- sympy.Rational: lambda q: TimeType.from_fraction(q.p, q.q),
+ sympy.Rational: lambda q: TimeType.from_fraction(int(q.p), int(q.q)),
TimeType: lambda x: x
}
@@ -351,13 +348,14 @@ def __new__(mcls, classname, bases, cls_dict):
for base in abstract_bases:
if name in base.__dict__ and name in base.__abstractmethods__:
+ base_name = base.__name__
base_member = getattr(base, name)
if member is base_member or not base_member.__doc__:
continue
base_member_name = '.'.join([base.__module__, base.__qualname__, name])
- member.__doc__ = 'Implements {}`~{}`.'.format(member_type, base_member_name)
+ member.__doc__ = f'Implements {member_type}`.{base_name}.{name}`.'
break
return cls
@@ -405,121 +403,7 @@ def has_type_interface(obj: typing.Any, type_obj: typing.Type) -> bool:
_T_co_hash = typing.TypeVar('_T_co_hash', bound=typing.Hashable, covariant=True) # Any type covariant containers.
FrozenMapping = typing.Mapping[_KT_hash, _T_co_hash]
-
-
-class _FrozenDictByInheritance(dict):
- """This is non mutable, hashable dict. It violates the Liskov substitution principle but is faster than wrapping.
- It is not used by default and may be removed in the future.
- """
- def __setitem__(self, key, value):
- raise TypeError('FrozenDict is immutable')
-
- def __delitem__(self, key):
- raise TypeError('FrozenDict is immutable')
-
- def update(self, *args, **kwargs):
- raise TypeError('FrozenDict is immutable')
-
- def setdefault(self, *args, **kwargs):
- raise TypeError('FrozenDict is immutable')
-
- def clear(self):
- raise TypeError('FrozenDict is immutable')
-
- def pop(self, *args, **kwargs):
- raise TypeError('FrozenDict is immutable')
-
- def popitem(self, *args, **kwargs):
- raise TypeError('FrozenDict is immutable')
-
- def copy(self):
- return self
-
- def to_dict(self) -> typing.Dict[_KT_hash, _T_co_hash]:
- return super().copy()
-
- def __hash__(self):
- # faster than functools.reduce(operator.xor, map(hash, self.items())) but takes more memory
- # TODO: investigate caching
- return hash(frozenset(self.items()))
-
-
-class _FrozenDictByWrapping(FrozenMapping):
- """Immutable dict like type.
-
- There are the following possibilities in pure python:
- - subclass dict (violates the Liskov substitution principle)
- - wrap dict (slow construction and method indirection)
- - abuse MappingProxyType (hard to add hash and make mutation difficult)
-
-
-
- Wrapper around builtin dict without the mutating methods.
-
- Hot path methods in __slots__ are the bound methods of the dict object. The other methods are wrappers.
-
- Why not subclass dict and overwrite mutating methods:
- roughly the same speed for __slot__ methods (a bit slower than native dict)
- dict subclass always implements MutableMapping which makes type annotations useless
- caching the hash value is slightly slower for the subclass
-
- Only downside: This wrapper class needs to implement __init__ and copy the __slot__ methods which is an overhead of
- ~10 i.e. 250ns for empty subclass init vs. 4µs for empty wrapper init
- """
- # made concessions in code style due to performance
- _HOT_PATH_METHODS = ('keys', 'items', 'values', 'get', '__getitem__')
- _PRIVATE_ATTRIBUTES = ('_hash', '_dict')
- __slots__ = _HOT_PATH_METHODS + _PRIVATE_ATTRIBUTES
-
- def __new__(cls, *args, **kwds):
- """Overwriting __new__ saves a factor of two for initialization. This is the relevant line from
- Generic.__new__"""
- return object.__new__(cls)
-
- def __init__(self, *args, **kwargs):
- inner_dict = dict(*args, **kwargs)
- self._dict = inner_dict # type: typing.Dict[_KT_hash, _T_co_hash]
- self._hash = None
-
- self.__getitem__ = inner_dict.__getitem__
- self.keys = inner_dict.keys
- self.items = inner_dict.items
- self.values = inner_dict.values
- self.get = inner_dict.get
-
- def __contains__(self, item: _KT_hash) -> bool:
- return item in self._dict
-
- def __iter__(self) -> typing.Iterator[_KT_hash]:
- return iter(self._dict)
-
- def __len__(self) -> int:
- return len(self._dict)
-
- def __repr__(self):
- return '%s(%r)' % (self.__class__.__name__, self._dict)
-
- def __hash__(self) -> int:
- # use the local variable h to minimize getattr calls to minimum and reduce caching overhead
- h = self._hash
- if h is None:
- self._hash = h = functools.reduce(operator.xor, map(hash, self.items()), 0xABCD0)
- return h
-
- def __eq__(self, other: typing.Mapping):
- return other == self._dict
-
- def copy(self):
- return self
-
- def to_dict(self) -> typing.Dict[_KT_hash, _T_co_hash]:
- return self._dict.copy()
-
-
-if frozendict is None:
- FrozenDict = _FrozenDictByWrapping
-else:
- FrozenDict = frozendict
+FrozenDict = frozendict
class SequenceProxy(collections.abc.Sequence):
@@ -556,5 +440,3 @@ def __eq__(self, other):
and all(x == y for x, y in zip(self, other)))
else:
return NotImplemented
-
-
diff --git a/readthedocs.yml b/readthedocs.yml
index 903c17d30..3c8203900 100644
--- a/readthedocs.yml
+++ b/readthedocs.yml
@@ -3,13 +3,14 @@ version: 2
build:
os: ubuntu-22.04
tools:
- python: "3.9"
+ python: "3.11"
python:
install:
- - requirements: doc/requirements.txt
- - method: setuptools
+ - method: pip
path: .
+ extra_requirements:
+ - default
sphinx:
builder: html
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 155a01e52..000000000
--- a/setup.cfg
+++ /dev/null
@@ -1,88 +0,0 @@
-[metadata]
-name = qupulse
-version = attr: qupulse.__version__
-description = A Quantum compUting PULse parametrization and SEquencing framework
-long_description = file: README.md
-long_description_content_type = text/markdown
-author = Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University
-license = MIT
-license_files = LICENSE
-keywords = quantum, physics, control pulse, qubit
-classifiers =
- Programming Language :: Python :: 3
- License :: OSI Approved :: MIT License
- Operating System :: OS Independent
- Topic :: Scientific/Engineering
- Intended Audience :: Science/Research
-url = https://github.com/qutech/qupulse
-
-[options]
-packages = find:
-package_dir =
- qupulse=qupulse
- qctoolkit=qctoolkit
-python_requires = >=3.7
-install_requires =
- sympy>=1.1.1
- numpy
- cached_property;python_version<'3.8'
- frozendict
-test_suite = tests
-
-[options.extras_require]
-tests =
- pytest
- pytest_benchmark
-docs =
- sphinx>=4
- nbsphinx
- ipykernel
- pyvisa
-plotting = matplotlib
-tabor-instruments =
- tabor_control>=0.1.1
-zurich-instruments = zhinst
-Faster-fractions = gmpy2
-tektronix = tek_awg>=0.2.1
-autologging = autologging
-# sadly not open source for external legal reasons
-# commented out because pypi does not allow direct dependencies
-# atsaverage = atsaverage @ git+ssh://git@git.rwth-aachen.de/qutech/cpp-atsaverage.git@master#egg=atsaverage&subdirectory=python_source
-faster-sampling = numba
-# Everything besides awg drivers
-default =
- pytest
- pytest_benchmark
- sphinx>=4
- ipykernel
- pyvisa
- matplotlib
- gmpy2
- autologging
- numba
- pandas
-
-[options.packages.find]
-include =
- qupulse
- qupulse.*
- qctoolkit
-
-[tool:pytest]
-testpaths = tests tests/pulses tests/hardware tests/backward_compatibility
-python_files=*_tests.py *_bug.py
-filterwarnings =
-# syntax is action:message_regex:category:module_regex:lineno
-# we fail on all with a whitelist because a dependency might mess-up passing the correct stacklevel
- error::SyntaxWarning
- error::DeprecationWarning
-# pytest uses readline which uses collections.abc
- ignore:Using or importing the ABCs from \'collections\' instead of from \'collections\.abc\' is deprecated:DeprecationWarning:.*readline.*
-
-[build_sphinx]
-project = 'qupulse'
-version = 0.5
-release = 0.5rc
-source-dir = ./doc/source
-build-dir = ./doc/build
-fresh-env = 1
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 5ff9a65c8..000000000
--- a/setup.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from setuptools import setup
-
-if __name__ == '__main__':
- # reads from setup.cfg
- setup()
diff --git a/tests/_program/loop_tests.py b/tests/_program/loop_tests.py
index 3bd811cd6..55d4e465f 100644
--- a/tests/_program/loop_tests.py
+++ b/tests/_program/loop_tests.py
@@ -11,12 +11,11 @@
from qupulse.parameter_scope import DictScope
from qupulse.utils.types import TimeType, time_from_float
-from qupulse._program.volatile import VolatileRepetitionCount
-from qupulse._program._loop import Loop, _make_compatible, _is_compatible, _CompatibilityLevel,\
+from qupulse.program.volatile import VolatileRepetitionCount
+from qupulse.program.loop import Loop, _make_compatible, _is_compatible, _CompatibilityLevel,\
RepetitionWaveform, SequenceWaveform, make_compatible, MakeCompatibleWarning, DroppedMeasurementWarning,\
VolatileModificationWarning, roll_constant_waveforms
-from qupulse._program._loop import Loop, _make_compatible, _is_compatible, _CompatibilityLevel,\
- RepetitionWaveform, SequenceWaveform, make_compatible, MakeCompatibleWarning, ConstantWaveform
+from qupulse.program.waveforms import *
from tests.pulses.sequencing_dummies import DummyWaveform
from qupulse.pulses.multi_channel_pulse_template import MultiChannelWaveform
@@ -142,7 +141,7 @@ def test_get_measurement_windows(self):
# no measurements left
self.assertEqual({}, prog.get_measurement_windows())
- def test_repr(self):
+ def test_str(self):
wf_gen = WaveformGenerator(num_channels=1)
wfs = [wf_gen() for _ in range(11)]
@@ -154,10 +153,19 @@ def test_repr(self):
loop.waveform = wfs.pop(0)
self.assertEqual(len(wfs), 0)
- self.assertEqual(repr(tree), expected)
+ self.assertEqual(str(tree), expected)
with mock.patch.object(Loop, 'MAX_REPR_SIZE', 1):
- self.assertEqual(repr(tree), '...')
+ self.assertEqual(str(tree), '...')
+
+ def test_repr(self):
+ root_loop = self.get_test_loop()
+
+ root_repr = repr(root_loop)
+
+ root_eval = eval(root_repr)
+
+ self.assertEqual(root_loop, root_eval)
def test_is_leaf(self):
root_loop = self.get_test_loop(waveform_generator=WaveformGenerator(1))
@@ -195,12 +203,12 @@ def test_flatten_and_balance(self):
after = before.copy_tree_structure()
after.flatten_and_balance(2)
- wf_reprs = dict(zip(ascii_uppercase,
- (repr(loop.waveform)
+ wf_strs = dict(zip(ascii_uppercase,
+ (str(loop.waveform)
for loop in before.get_depth_first_iterator()
if loop.is_leaf())))
- before_repr = """\
+ before_str = """\
LOOP 1 times:
->EXEC {A} 1 times
->LOOP 10 times:
@@ -220,10 +228,10 @@ def test_flatten_and_balance(self):
->EXEC {I} 8 times
->LOOP 9 times:
->EXEC {J} 10 times
- ->EXEC {K} 11 times""".format(**wf_reprs)
- self.assertEqual(repr(before), before_repr)
+ ->EXEC {K} 11 times""".format(**wf_strs)
+ self.assertEqual(str(before), before_str)
- expected_after_repr = """\
+ expected_after_str = """\
LOOP 1 times:
->LOOP 1 times:
->EXEC {A} 1 times
@@ -261,9 +269,9 @@ def test_flatten_and_balance(self):
->EXEC {I} 8 times
->LOOP 9 times:
->EXEC {J} 10 times
- ->EXEC {K} 11 times""".format(**wf_reprs)
+ ->EXEC {K} 11 times""".format(**wf_strs)
- self.assertEqual(expected_after_repr, repr(after))
+ self.assertEqual(expected_after_str, str(after))
def test_flatten_and_balance_comparison_based(self):
wfs = [DummyWaveform(duration=i) for i in range(2)]
@@ -479,27 +487,27 @@ def test_make_compatible(self):
sample_rate=TimeType.from_float(1.))
priv_kwargs = dict(min_len=5, quantum=10, sample_rate=TimeType.from_float(1.))
- with mock.patch('qupulse._program._loop._is_compatible',
+ with mock.patch('qupulse.program.loop._is_compatible',
return_value=_CompatibilityLevel.incompatible_too_short) as mocked:
with self.assertRaisesRegex(ValueError, 'too short'):
make_compatible(program, **pub_kwargs)
mocked.assert_called_once_with(program, **priv_kwargs)
- with mock.patch('qupulse._program._loop._is_compatible',
+ with mock.patch('qupulse.program.loop._is_compatible',
return_value=_CompatibilityLevel.incompatible_fraction) as mocked:
with self.assertRaisesRegex(ValueError, 'not an integer'):
make_compatible(program, **pub_kwargs)
mocked.assert_called_once_with(program, **priv_kwargs)
- with mock.patch('qupulse._program._loop._is_compatible',
+ with mock.patch('qupulse.program.loop._is_compatible',
return_value=_CompatibilityLevel.incompatible_quantum) as mocked:
with self.assertRaisesRegex(ValueError, 'not a multiple of quantum'):
make_compatible(program, **pub_kwargs)
mocked.assert_called_once_with(program, **priv_kwargs)
- with mock.patch('qupulse._program._loop._is_compatible',
+ with mock.patch('qupulse.program.loop._is_compatible',
return_value=_CompatibilityLevel.action_required) as is_compat:
- with mock.patch('qupulse._program._loop._make_compatible') as make_compat:
+ with mock.patch('qupulse.program.loop._make_compatible') as make_compat:
make_compatible(program, **pub_kwargs)
is_compat.assert_called_once_with(program, **priv_kwargs)
diff --git a/tests/_program/seqc_tests.py b/tests/_program/seqc_tests.py
deleted file mode 100644
index 6b2cfb0db..000000000
--- a/tests/_program/seqc_tests.py
+++ /dev/null
@@ -1,1067 +0,0 @@
-import unittest
-from unittest import TestCase, mock
-import time
-from itertools import zip_longest, islice
-import sys
-import tempfile
-import pathlib
-import hashlib
-import random
-
-import numpy as np
-
-from qupulse.expressions import ExpressionScalar
-from qupulse.parameter_scope import DictScope
-
-from qupulse._program._loop import Loop
-from qupulse._program.waveforms import ConstantWaveform
-from qupulse._program.seqc import BinaryWaveform, loop_to_seqc, WaveformPlayback, Repeat, SteppingRepeat, Scope,\
- to_node_clusters, find_sharable_waveforms, mark_sharable_waveforms, UserRegisterManager, HDAWGProgramManager,\
- UserRegister, WaveformFileSystem
-from qupulse._program.volatile import VolatileRepetitionCount
-
-from tests.pulses.sequencing_dummies import DummyWaveform
-
-try:
- import zhinst
-except ImportError:
- zhinst = None
-
-
-def take(n, iterable):
- "Return first n items of the iterable as a list"
- return list(islice(iterable, n))
-
-
-def dummy_loop_to_seqc(loop, **kwargs):
- return loop
-
-
-class BinaryWaveformTest(unittest.TestCase):
- MAX_RATE = 14
-
- def test_dynamic_rate_reduction(self):
-
- ones = np.ones(2**(self.MAX_RATE + 2) * 3, np.uint16)
-
- for n in (2, 3, 5):
- self.assertEqual(BinaryWaveform(ones[:n * 16 * 3]).dynamic_rate(), 0, f"Reducing {n}")
- for n in (4, 6):
- self.assertEqual(BinaryWaveform(ones[:16 * n * 3]).dynamic_rate(), 1)
-
- irreducibles = [
- np.array([0, 0, 1, 1, 0, 1] * 16, dtype=np.uint16),
- np.array([0, 0, 0] * 16 + [0, 1, 0] + [0, 0, 0] * 15, dtype=np.uint16),
- np.array([0, 0, 0] * 16 + [1, 0, 0] + [0, 0, 0] * 15, dtype=np.uint16),
- ]
- for max_rate in range(self.MAX_RATE):
- for n in range(self.MAX_RATE):
- for irreducible in irreducibles:
- data = np.tile(np.tile(irreducible.reshape(-1, 1, 3), (1, 2**n, 1)).ravel(), (16,))
-
- dyn_n = BinaryWaveform(data).dynamic_rate(max_rate=max_rate)
-
- self.assertEqual(min(max_rate, n), dyn_n)
-
-
-def make_binary_waveform(waveform):
- if zhinst is None:
- # TODO: mock used function
- raise unittest.SkipTest("zhinst not present")
-
- if waveform.duration == 0:
- data = np.asarray(3 * [1, 2, 3, 4, 5], dtype=np.uint16)
- return (BinaryWaveform(data),)
- else:
- chs = sorted(waveform.defined_channels)
- t = np.arange(0., float(waveform.duration), 1.)
-
- sampled = [None if ch is None else waveform.get_sampled(ch, t)
- for _, ch in zip_longest(range(6), take(6, chs), fillvalue=None)]
- ch1, ch2, *markers = sampled
- return (BinaryWaveform.from_sampled(ch1, ch2, markers),)
-
-
-def _key_to_int(n: int, duration: int, defined_channels: frozenset):
- key_bytes = str((n, duration, sorted(defined_channels))).encode('ascii')
- key_int64 = int(hashlib.sha256(key_bytes).hexdigest()[:2*8], base=16) // 2
- return key_int64
-
-
-def get_unique_wfs(n=10000, duration=32, defined_channels=frozenset(['A'])):
- if not hasattr(get_unique_wfs, 'cache'):
- get_unique_wfs.cache = {}
-
- key = (n, duration, defined_channels)
-
- if key not in get_unique_wfs.cache:
- # positive deterministic int64
- h = _key_to_int(n, duration, defined_channels)
- base = np.bitwise_xor(np.linspace(-h, h, num=duration + n, dtype=np.int64), h)
- base = base / np.max(np.abs(base))
-
- get_unique_wfs.cache[key] = [
- DummyWaveform(duration=duration, sample_output=base[idx:idx+duration],
- defined_channels=defined_channels)
- for idx in range(n)
- ]
- return get_unique_wfs.cache[key]
-
-
-def get_constant_unique_wfs(n=10000, duration=192, defined_channels=frozenset(['A'])):
- if not hasattr(get_unique_wfs, 'cache'):
- get_unique_wfs.cache = {}
-
- key = (n, duration, defined_channels)
-
- if key not in get_unique_wfs.cache:
- bit_gen = np.random.PCG64(_key_to_int(n, duration, defined_channels))
- rng = np.random.Generator(bit_gen)
-
- random_values = rng.random(size=(n, len(defined_channels)))
-
- sorted_channels = sorted(defined_channels)
- get_unique_wfs.cache[key] = [
- ConstantWaveform.from_mapping(duration, {ch: ch_value
- for ch, ch_value in zip(sorted_channels, wf_values)})
- for wf_values in random_values
- ]
- return get_unique_wfs.cache[key]
-
-
-def complex_program_as_loop(unique_wfs, wf_same):
- root = Loop(repetition_count=12)
-
- for wf_unique in unique_wfs:
- root.append_child(children=[Loop(repetition_count=42, waveform=wf_unique),
- Loop(repetition_count=98, waveform=wf_same)],
- repetition_count=10)
-
- root.append_child(waveform=unique_wfs[0], repetition_count=21)
- root.append_child(waveform=wf_same, repetition_count=23)
-
- volatile_repetition = VolatileRepetitionCount(ExpressionScalar('n + 4'),
- DictScope.from_kwargs(n=3, volatile={'n'}))
- root.append_child(waveform=wf_same, repetition_count=volatile_repetition)
-
- return root
-
-
-def complex_program_as_seqc(unique_wfs, wf_same):
- return Repeat(12,
- Scope([
- SteppingRepeat([
- Repeat(repetition_count=10, scope=Scope([
- Repeat(42, WaveformPlayback(make_binary_waveform(unique_wf))),
- Repeat(98, WaveformPlayback(make_binary_waveform(wf_same), shared=True)),
- ]))
- for unique_wf in unique_wfs
- ]),
- Repeat(21, WaveformPlayback(make_binary_waveform(unique_wfs[0]))),
- Repeat(23, WaveformPlayback(make_binary_waveform(wf_same))),
- Repeat('test_14', WaveformPlayback(make_binary_waveform(wf_same)))
- ])
- )
-
-
-class DummyWfManager:
- def __init__(self):
- self.shared = {}
- self.concatenated = []
-
- def request_shared(self, wf):
- return self.shared.setdefault(wf, len(self.shared) + 1)
-
- def request_concatenated(self, wf):
- self.concatenated.append(wf)
- return 0
-
-
-class WaveformFileSystemTests(TestCase):
- def setUp(self) -> None:
- clients = [mock.Mock(), mock.Mock()]
- bin_waveforms = [mock.Mock(), mock.Mock(), mock.Mock()]
- table_data = [np.ones(1, dtype=np.uint16) * i for i, _ in enumerate(bin_waveforms)]
- for bin_wf, tab in zip(bin_waveforms, table_data):
- bin_wf.to_csv_compatible_table.return_value = tab
-
- self.temp_dir = tempfile.TemporaryDirectory()
- self.table_data = table_data
- self.clients = clients
- self.waveforms = [
- {'0': bin_waveforms[0], '1': bin_waveforms[1]},
- {'1': bin_waveforms[1], '2': bin_waveforms[2]}
- ]
- self.fs = WaveformFileSystem(pathlib.Path(self.temp_dir.name))
-
- def read_files(self) -> dict:
- return {
- p.name: p.read_text().strip() for p in self.fs._path.iterdir()
- }
-
- def tearDown(self) -> None:
- self.temp_dir.cleanup()
-
- def test_pub_sync(self):
- with mock.patch.object(self.fs, '_sync') as mock_sync:
- self.fs.sync(self.clients[0], self.waveforms[0], hallo=0)
- mock_sync.assert_called_once_with(hallo=0)
-
- self.assertEqual({id(self.clients[0]): self.waveforms[0]}, self.fs._required)
-
- def test_sync(self):
- self.fs.sync(self.clients[0], self.waveforms[0])
- self.assertEqual({'0': '0', '1': '1'}, self.read_files())
-
- self.fs.sync(self.clients[0], self.waveforms[1])
- self.assertEqual({'2': '2', '1': '1'}, self.read_files())
-
- self.fs.sync(self.clients[1], self.waveforms[0])
- self.assertEqual({'2': '2', '1': '1', '0': '0'}, self.read_files())
-
- def test_sync_write_all(self):
- self.fs.sync(self.clients[0], self.waveforms[0])
- self.assertEqual({'0': '0', '1': '1'}, self.read_files())
-
- self.table_data[0][:] = 7
- self.fs.sync(self.clients[0], self.waveforms[0])
- self.assertEqual({'0': '0', '1': '1'}, self.read_files())
-
- self.fs.sync(self.clients[0], self.waveforms[0], write_all=True)
- self.assertEqual({'0': '7', '1': '1'}, self.read_files())
-
- def test_sync_no_delete(self):
- self.fs.sync(self.clients[0], self.waveforms[0])
- self.assertEqual({'0': '0', '1': '1'}, self.read_files())
-
- self.fs.sync(self.clients[0], self.waveforms[1], delete=False)
- self.assertEqual({'2': '2', '1': '1', '0': '0'}, self.read_files())
-
-
-class SEQCNodeTests(TestCase):
- """Test everything besides source code generation"""
- @unittest.skipIf(zhinst is None, "test requires zhinst")
- def test_visit_nodes(self):
- wf, wf_shared = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(2)))
- wf_shared.shared = True
-
- waveform_manager = mock.Mock(wraps=DummyWfManager())
- wf._visit_nodes(waveform_manager)
- waveform_manager.request_concatenated.assert_called_once_with(wf.waveform)
-
- waveform_manager = mock.Mock(wraps=DummyWfManager())
- wf_shared._visit_nodes(waveform_manager)
- waveform_manager.request_concatenated.assert_not_called()
-
- scope = Scope([mock.Mock(wraps=wf), mock.Mock(wraps=wf_shared)])
- scope._visit_nodes(waveform_manager)
- scope.nodes[0]._visit_nodes.assert_called_once_with(waveform_manager)
- scope.nodes[1]._visit_nodes.assert_called_once_with(waveform_manager)
- waveform_manager.request_concatenated.assert_called_once_with(wf.waveform)
-
- waveform_manager = mock.Mock(wraps=DummyWfManager())
- repeat = Repeat(12, mock.Mock(wraps=wf))
- repeat._visit_nodes(waveform_manager)
- repeat.scope._visit_nodes.assert_called_once_with(waveform_manager)
- waveform_manager.request_concatenated.assert_called_once_with(wf.waveform)
-
- waveform_manager = mock.Mock(wraps=DummyWfManager())
- stepping_repeat = SteppingRepeat([mock.Mock(wraps=wf), mock.Mock(wraps=wf), mock.Mock(wraps=wf)])
- stepping_repeat._visit_nodes(waveform_manager)
- for node in stepping_repeat.node_cluster:
- node._visit_nodes.assert_called_once_with(waveform_manager)
-
- @unittest.skipIf(zhinst is None, "test requires zhinst")
- def test_same_stepping(self):
- wf1, wf2 = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(2, 32)))
- wf3, wf_shared = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(2, 64)))
- wf_shared.shared = True
-
- scope1 = Scope([wf1, wf1, wf2])
- scope2 = Scope([wf1, wf2, wf2])
- scope3 = Scope([wf1, wf2, wf3])
- scope4 = Scope([wf1, wf2, wf2, wf2])
-
- repeat1 = Repeat(13, wf1)
- repeat2 = Repeat(13, wf2)
- repeat3 = Repeat(15, wf2)
- repeat4 = Repeat(13, wf3)
-
- stepping_repeat1 = SteppingRepeat([wf1, wf1, wf2])
- stepping_repeat2 = SteppingRepeat([wf2, wf2, wf2])
- stepping_repeat3 = SteppingRepeat([wf3, wf3, wf3])
- stepping_repeat4 = SteppingRepeat([wf1, wf1, wf2, wf1])
-
- self.assertTrue(wf1.same_stepping(wf1))
- self.assertTrue(wf1.same_stepping(wf2))
- self.assertFalse(wf1.same_stepping(wf3))
- self.assertFalse(wf3.same_stepping(wf_shared))
- self.assertFalse(wf_shared.same_stepping(wf3))
-
- self.assertFalse(scope1.same_stepping(wf1))
- self.assertTrue(scope1.same_stepping(scope2))
- self.assertFalse(scope1.same_stepping(scope3))
- self.assertFalse(scope1.same_stepping(scope4))
-
- self.assertFalse(repeat1.same_stepping(scope1))
- self.assertTrue(repeat1.same_stepping(repeat2))
- self.assertFalse(repeat1.same_stepping(repeat3))
- self.assertFalse(repeat1.same_stepping(repeat4))
-
- self.assertFalse(stepping_repeat1.same_stepping(scope1))
- self.assertTrue(stepping_repeat1.same_stepping(stepping_repeat2))
- self.assertFalse(stepping_repeat1.same_stepping(stepping_repeat3))
- self.assertFalse(stepping_repeat1.same_stepping(stepping_repeat4))
-
- @unittest.skipIf(zhinst is None, "test requires zhinst")
- def test_iter_waveform_playback(self):
- wf1, wf2 = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(2, 32)))
- wf3, wf_shared = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(2, 64)))
-
- for wf in (wf1, wf2, wf3, wf_shared):
- pb, = wf.iter_waveform_playbacks()
- self.assertIs(pb, wf)
-
- repeat = Repeat(13, wf1)
- self.assertEqual(list(repeat.iter_waveform_playbacks()), [wf1])
-
- scope = Scope([wf1, repeat, wf2, wf3, wf_shared])
- self.assertEqual(list(scope.iter_waveform_playbacks()), [wf1, wf1, wf2, wf3, wf_shared])
-
- stepping_repeat = SteppingRepeat([wf1, repeat, wf2, wf3, wf_shared])
- self.assertEqual(list(stepping_repeat.iter_waveform_playbacks()), [wf1, wf1, wf2, wf3, wf_shared])
-
- @unittest.skipIf(zhinst is None, "test requires zhinst")
- def test_get_single_indexed_playback(self):
- wf1, wf_shared = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(2, 32)))
- wf_shared.shared = True
- self.assertIs(wf1._get_single_indexed_playback(), wf1)
- self.assertIsNone(wf_shared._get_single_indexed_playback())
-
- self.assertIs(Scope([wf1, wf_shared])._get_single_indexed_playback(), wf1)
- self.assertIsNone(Scope([wf1, wf_shared, wf1])._get_single_indexed_playback(), wf1)
-
- def test_get_position_advance_strategy(self):
- node = mock.Mock()
- node.samples.return_value = 0
- node._get_single_indexed_playback.return_value.samples.return_value = 128
- repeat = Repeat(10, node)
-
- # no samples at all
- self.assertIs(repeat._get_position_advance_strategy(), repeat._AdvanceStrategy.IGNORE)
- node.samples.assert_called_once_with()
- node._get_single_indexed_playback.assert_not_called()
-
- node.reset_mock()
- node.samples.return_value = 64
-
- # samples do differ
- self.assertIs(repeat._get_position_advance_strategy(), repeat._AdvanceStrategy.INITIAL_RESET)
- node.samples.assert_called_once_with()
- node._get_single_indexed_playback.assert_called_once_with()
- node._get_single_indexed_playback.return_value.samples.assert_called_once_with()
-
- node.reset_mock()
- node.samples.return_value = 128
-
- # samples are the same
- self.assertIs(repeat._get_position_advance_strategy(), repeat._AdvanceStrategy.POST_ADVANCE)
- node.samples.assert_called_once_with()
- node._get_single_indexed_playback.assert_called_once_with()
- node._get_single_indexed_playback.return_value.samples.assert_called_once_with()
-
- node.reset_mock()
- node._get_single_indexed_playback.return_value = None
- # multiple indexed playbacks
- self.assertIs(repeat._get_position_advance_strategy(), repeat._AdvanceStrategy.INITIAL_RESET)
- node.samples.assert_called_once_with()
- node._get_single_indexed_playback.assert_called_once_with()
-
-
-@unittest.skipIf(zhinst is None, "test requires zhinst")
-class LoopToSEQCTranslationTests(TestCase):
- def test_loop_to_seqc_leaf(self):
- """Test the translation of leaves"""
- # we use None because it is not used in this test
- user_registers = None
-
- wf = DummyWaveform(duration=32, sample_output=lambda x: np.sin(x))
- loop = Loop(waveform=wf)
-
- # with wrapping repetition
- loop.repetition_count = 15
- waveform_to_bin = mock.Mock(wraps=make_binary_waveform)
- expected = Repeat(loop.repetition_count, WaveformPlayback(waveform=make_binary_waveform(wf)))
- result = loop_to_seqc(loop, 1, 1, waveform_to_bin, user_registers=user_registers)
- waveform_to_bin.assert_called_once_with(wf)
- self.assertEqual(expected, result)
-
- # without wrapping repetition
- loop.repetition_count = 1
- waveform_to_bin = mock.Mock(wraps=make_binary_waveform)
- expected = WaveformPlayback(waveform=make_binary_waveform(wf))
- result = loop_to_seqc(loop, 1, 1, waveform_to_bin, user_registers=user_registers)
- waveform_to_bin.assert_called_once_with(wf)
- self.assertEqual(expected, result)
-
- def test_loop_to_seqc_len_1(self):
- """Test the translation of loops with len(loop) == 1"""
- # we use None because it is not used in this test
- user_registers = None
-
- loop = Loop(children=[Loop()])
- waveform_to_bin = mock.Mock(wraps=make_binary_waveform)
- loop_to_seqc_kwargs = dict(min_repetitions_for_for_loop=2,
- min_repetitions_for_shared_wf=3,
- waveform_to_bin=waveform_to_bin,
- user_registers=user_registers)
-
- expected = 'asdf'
- with mock.patch('qupulse._program.seqc.loop_to_seqc', return_value=expected) as mocked_loop_to_seqc:
- result = loop_to_seqc(loop, **loop_to_seqc_kwargs)
- self.assertEqual(result, expected)
- mocked_loop_to_seqc.assert_called_once_with(loop[0], **loop_to_seqc_kwargs)
-
- loop.repetition_count = 14
- expected = Repeat(14, 'asdfg')
- with mock.patch('qupulse._program.seqc.loop_to_seqc', return_value=expected.scope) as mocked_loop_to_seqc:
- result = loop_to_seqc(loop, **loop_to_seqc_kwargs)
- self.assertEqual(result, expected)
- mocked_loop_to_seqc.assert_called_once_with(loop[0], **loop_to_seqc_kwargs)
-
- waveform_to_bin.assert_not_called()
-
- def test_to_node_clusters(self):
- """Test cluster generation"""
- wf1, wf2 = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(2, 32)))
- wf3, wf_shared = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(2, 64)))
-
- loop_to_seqc_kwargs = {'my': 'kwargs'}
-
- loops = [wf1, wf2, wf1, wf1, wf3, wf1, wf1, wf1, wf3, wf1, wf3, wf1, wf3]
- expected_calls = [mock.call(loop, **loop_to_seqc_kwargs) for loop in loops]
- expected_result = [[wf1, wf2, wf1, wf1], [wf3], [wf1, wf1, wf1], [Scope([wf3, wf1]), Scope([wf3, wf1])], [wf3]]
-
- with mock.patch('qupulse._program.seqc.loop_to_seqc', wraps=dummy_loop_to_seqc) as mock_loop_to_seqc:
- result = to_node_clusters(loops, loop_to_seqc_kwargs)
- self.assertEqual(mock_loop_to_seqc.mock_calls, expected_calls)
- self.assertEqual(expected_result, result)
-
- def test_to_node_clusters_crash(self):
- wf1 = WaveformPlayback(make_binary_waveform(*get_unique_wfs(1, 32)))
- wf2 = WaveformPlayback(make_binary_waveform(*get_unique_wfs(1, 64)))
- wf3 = WaveformPlayback(make_binary_waveform(*get_unique_wfs(1, 128)))
- wf4 = WaveformPlayback(make_binary_waveform(*get_unique_wfs(1, 256)))
-
- loop_to_seqc_kwargs = {'my': 'kwargs'}
-
- loops = [wf1, wf2, wf3] * 3 + [wf1] + [wf2, wf4] * 3 + [wf1]
- with mock.patch('qupulse._program.seqc.loop_to_seqc', wraps=dummy_loop_to_seqc) as mock_loop_to_seqc:
- result = to_node_clusters(loops, loop_to_seqc_kwargs)
- expected_result = [[Scope([wf1, wf2, wf3])]*3, [wf1], [Scope([wf2, wf4])]*3, [wf1]]
- self.assertEqual(expected_result, result)
-
- def test_find_sharable_waveforms(self):
- wf1, wf2 = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(2, 32)))
- wf3, wf_shared = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(2, 64)))
-
- scope1 = Scope([wf1, wf1, wf_shared, wf1])
- scope2 = Scope([wf1, wf2, wf_shared, wf2])
- scope3 = Scope([wf2, wf2, wf_shared, wf3])
- scope4 = Scope([wf2, wf2, wf3, wf3])
-
- self.assertIsNone(find_sharable_waveforms([scope1, scope2, scope3, scope4]))
-
- shareable = find_sharable_waveforms([scope1, scope2, scope3])
- self.assertEqual([False, False, True, False], shareable)
-
- def test_mark_sharable_waveforms(self):
- shareable = [False, False, True, False]
-
- pb_gen = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(12, 32)))
-
- nodes = [Scope([mock.Mock(wraps=next(pb_gen)), mock.Mock(wraps=next(pb_gen)), mock.Mock(wraps=next(pb_gen)), mock.Mock(wraps=next(pb_gen))]),
- Scope([mock.Mock(wraps=next(pb_gen)), mock.Mock(wraps=next(pb_gen)), mock.Mock(wraps=next(pb_gen)), mock.Mock(wraps=next(pb_gen))]),
- Scope([mock.Mock(wraps=next(pb_gen)), mock.Mock(wraps=next(pb_gen)), mock.Mock(wraps=next(pb_gen)), mock.Mock(wraps=next(pb_gen))])]
-
- mocks = [mock.Mock(wraps=scope) for scope in nodes]
-
- mark_sharable_waveforms(mocks, shareable)
-
- for mock_scope, scope in zip(mocks, nodes):
- mock_scope.iter_waveform_playbacks.assert_called_once_with()
- m1, m2, m3, m4 = scope.nodes
- self.assertIsInstance(m1.shared, mock.Mock)
- m1.iter_waveform_playbacks.assert_called_once_with()
- self.assertIsInstance(m2.shared, mock.Mock)
- m2.iter_waveform_playbacks.assert_called_once_with()
- self.assertTrue(m3.shared)
- m3.iter_waveform_playbacks.assert_called_once_with()
- self.assertIsInstance(m4.shared, mock.Mock)
- m4.iter_waveform_playbacks.assert_called_once_with()
-
- def test_loop_to_seqc_cluster_handling(self):
- """Test handling of clusters"""
-
- # we use None because it is not used in this test
- user_registers = None
-
- with self.assertRaises(AssertionError):
- loop_to_seqc(Loop(repetition_count=12, children=[Loop()]),
- min_repetitions_for_for_loop=3, min_repetitions_for_shared_wf=2,
- waveform_to_bin=make_binary_waveform, user_registers=user_registers)
-
- loop_to_seqc_kwargs = dict(min_repetitions_for_for_loop=3,
- min_repetitions_for_shared_wf=4,
- waveform_to_bin=make_binary_waveform, user_registers=user_registers)
-
- wf_same = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(100000, 32)))
- wf_sep, = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(1, 64)))
-
- node_clusters = [take(2, wf_same), [wf_sep],
- take(3, wf_same), [wf_sep],
- take(4, wf_same), take(4, wf_same)]
- root = Loop(repetition_count=12, children=[Loop() for _ in range(2 + 1 + 3 + 1 + 4 + 1 + 4)])
-
- expected = Repeat(12, Scope([
- *node_clusters[0],
- wf_sep,
- SteppingRepeat(node_clusters[2]),
- wf_sep,
- SteppingRepeat(node_clusters[4]),
- SteppingRepeat(node_clusters[5])
- ]))
-
- def dummy_find_sharable_waveforms(cluster):
- if cluster is node_clusters[4]:
- return [True]
- else:
- return None
-
- p1 = mock.patch('qupulse._program.seqc.to_node_clusters', return_value=node_clusters)
- p2 = mock.patch('qupulse._program.seqc.find_sharable_waveforms', wraps=dummy_find_sharable_waveforms)
- p3 = mock.patch('qupulse._program.seqc.mark_sharable_waveforms')
-
- with p1 as to_node_clusters_mock, p2 as find_share_mock, p3 as mark_share_mock:
- result = loop_to_seqc(root, **loop_to_seqc_kwargs)
- self.assertEqual(expected, result)
-
- to_node_clusters_mock.assert_called_once_with(root, loop_to_seqc_kwargs)
- self.assertEqual(find_share_mock.mock_calls,
- [mock.call(node_clusters[4]), mock.call(node_clusters[5])])
- mark_share_mock.assert_called_once_with(node_clusters[4], [True])
-
- def test_program_translation(self):
- """Integration test"""
- user_registers = UserRegisterManager(range(14, 15), 'test_{register}')
-
- unique_wfs = get_unique_wfs()
- same_wf = DummyWaveform(duration=32, sample_output=np.ones(32))
- root = complex_program_as_loop(unique_wfs, wf_same=same_wf)
-
- t0 = time.perf_counter()
-
- seqc = loop_to_seqc(root, 50, 100, make_binary_waveform, user_registers=user_registers)
-
- t1 = time.perf_counter()
- print('took', t1 - t0, 's')
-
- expected = complex_program_as_seqc(unique_wfs, wf_same=same_wf)
- self.assertEqual(expected, seqc)
-
-
-@unittest.skipIf(zhinst is None, "test requires zhinst")
-class SEQCToCodeTranslationTests(TestCase):
- def setUp(self) -> None:
- self.line_prefix = ' '
- self.node_name_generator = map(str, range(10000000000000000000))
- self.pos_var_name = 'foo'
- self.waveform_manager = DummyWfManager()
-
- def test_shared_playback(self):
- wf, = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(1, 32)))
- wf.shared = True
-
- expected = [' playWave(1);']
- result = list(wf.to_source_code(self.waveform_manager, self.node_name_generator, self.line_prefix, self.pos_var_name, True))
- self.assertEqual(expected, result)
-
- def test_indexed_playback(self):
- wf, = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(1, 32)))
-
- expected = [' playWaveIndexed(0, foo, 32); foo = foo + 32;']
- result = list(
- wf.to_source_code(self.waveform_manager, self.node_name_generator, self.line_prefix, self.pos_var_name,
- True))
- self.assertEqual(expected, result)
-
- expected = [' playWaveIndexed(0, foo, 32);' + wf.ADVANCE_DISABLED_COMMENT]
- result = list(
- wf.to_source_code(self.waveform_manager, self.node_name_generator, self.line_prefix, self.pos_var_name,
- False))
- self.assertEqual(expected, result)
-
- def test_scope(self):
- nodes = [mock.Mock(), mock.Mock(), mock.Mock()]
- for idx, node in enumerate(nodes):
- node.to_source_code = mock.Mock(return_value=map(str, [idx + 100, idx + 200]))
-
- scope = Scope(nodes)
- expected = ['100', '200', '101', '201', '102', '202']
- result = list(scope.to_source_code(self.waveform_manager, self.node_name_generator,
- self.line_prefix, self.pos_var_name, False))
- self.assertEqual(expected, result)
- for node in nodes:
- node.to_source_code.assert_called_once_with(self.waveform_manager,
- line_prefix=self.line_prefix,
- pos_var_name=self.pos_var_name,
- node_name_generator=self.node_name_generator,
- advance_pos_var=False)
-
- def test_stepped_repeat(self):
- nodes = [mock.Mock(), mock.Mock(), mock.Mock()]
- for idx, node in enumerate(nodes):
- node.to_source_code = mock.Mock(return_value=map(str, [idx + 100, idx + 200]))
-
- stepping_repeat = SteppingRepeat(nodes)
-
- body_prefix = self.line_prefix + stepping_repeat.INDENTATION
- expected = [
- ' repeat(3) {' + stepping_repeat.STEPPING_REPEAT_COMMENT,
- '100',
- '200',
- ' }'
- ]
- result = list(stepping_repeat.to_source_code(self.waveform_manager, self.node_name_generator,
- self.line_prefix, self.pos_var_name, False))
- self.assertEqual(expected, result)
- nodes[0].to_source_code.assert_called_once_with(self.waveform_manager,
- line_prefix=body_prefix,
- pos_var_name=self.pos_var_name,
- node_name_generator=self.node_name_generator,
- advance_pos_var=False)
- nodes[1].to_source_code.assert_not_called()
- nodes[2].to_source_code.assert_not_called()
- nodes[0]._visit_nodes.assert_not_called()
- nodes[1]._visit_nodes.assert_called_once_with(self.waveform_manager)
- nodes[2]._visit_nodes.assert_called_once_with(self.waveform_manager)
-
- def test_repeat(self):
- node = mock.Mock()
- node.to_source_code = mock.Mock(return_value=['asd', 'jkl'])
- node._get_single_indexed_playback = mock.Mock(return_value=None)
- node.samples = mock.Mock(return_value=64)
-
- repeat = Repeat(12, node)
-
- body_prefix = self.line_prefix + repeat.INDENTATION
- expected = [' var init_pos_0 = foo;',
- ' repeat(12) {',
- ' foo = init_pos_0;',
- 'asd', 'jkl', ' }']
-
- result = list(repeat.to_source_code(self.waveform_manager,
- node_name_generator=self.node_name_generator,
- line_prefix=self.line_prefix, pos_var_name=self.pos_var_name,
- advance_pos_var=True))
- self.assertEqual(expected, result)
- node.to_source_code.assert_called_once_with(self.waveform_manager, node_name_generator=self.node_name_generator,
- line_prefix=body_prefix,
- pos_var_name=self.pos_var_name,
- advance_pos_var=True)
- node._get_single_indexed_playback.assert_called_once_with()
- node.samples.assert_called_once_with()
-
- def test_repeat_detect_no_advance(self):
- node = mock.Mock()
- node.to_source_code = mock.Mock(return_value=['asd', 'jkl'])
- node._get_single_indexed_playback = mock.Mock(return_value=None)
- node.samples = mock.Mock(return_value=0)
-
- repeat = Repeat(12, node)
- body_prefix = self.line_prefix + repeat.INDENTATION
-
- expected = [' repeat(12) {',
- 'asd', 'jkl', ' }']
- result_no_advance = list(repeat.to_source_code(self.waveform_manager,
- node_name_generator=self.node_name_generator,
- line_prefix=self.line_prefix, pos_var_name=self.pos_var_name,
- advance_pos_var=True))
- self.assertEqual(expected, result_no_advance)
- node.to_source_code.assert_called_once_with(self.waveform_manager, node_name_generator=self.node_name_generator,
- line_prefix=body_prefix,
- pos_var_name=self.pos_var_name,
- advance_pos_var=False)
- node._get_single_indexed_playback.assert_not_called()
- node.samples.assert_called_once_with()
-
- def test_repeat_extern_no_advance(self):
- node = mock.Mock()
- node.to_source_code = mock.Mock(return_value=['asd', 'jkl'])
- node._get_single_indexed_playback = mock.Mock(return_value=None)
- node.samples = mock.Mock(return_value=64)
-
- repeat = Repeat(12, node)
-
- body_prefix = self.line_prefix + repeat.INDENTATION
-
- expected = [' repeat(12) {',
- 'asd', 'jkl', ' }']
- result_no_advance = list(repeat.to_source_code(self.waveform_manager,
- node_name_generator=self.node_name_generator,
- line_prefix=self.line_prefix, pos_var_name=self.pos_var_name,
- advance_pos_var=False))
- self.assertEqual(expected, result_no_advance)
- node.to_source_code.assert_called_once_with(self.waveform_manager, node_name_generator=self.node_name_generator,
- line_prefix=body_prefix,
- pos_var_name=self.pos_var_name,
- advance_pos_var=False)
- node._get_single_indexed_playback.assert_not_called()
- node.samples.assert_not_called()
-
- def test_program_to_code_translation(self):
- """Integration test"""
- unique_wfs = get_unique_wfs()
- same_wf = DummyWaveform(duration=48, sample_output=np.ones(48))
- seqc_nodes = complex_program_as_seqc(unique_wfs, wf_same=same_wf)
-
- wf_manager = DummyWfManager()
- def node_name_gen():
- for i in range(100):
- yield str(i)
-
- seqc_code = '\n'.join(seqc_nodes.to_source_code(wf_manager,
- line_prefix='',
- pos_var_name='pos',
- node_name_generator=node_name_gen()))
- # this is just copied from the result...
- expected = """var init_pos_0 = pos;
-repeat(12) {
- pos = init_pos_0;
- repeat(10000) { // stepping repeat
- repeat(10) {
- repeat(42) {
- playWaveIndexed(0, pos, 32); // advance disabled do to parent repetition
- }
- repeat(98) {
- playWave(1);
- }
- }
- pos = pos + 32;
- }
- repeat(21) {
- playWaveIndexed(0, pos, 32); // advance disabled do to parent repetition
- }
- pos = pos + 32;
- repeat(23) {
- playWaveIndexed(0, pos, 48); // advance disabled do to parent repetition
- }
- pos = pos + 48;
- var idx_1;
- for(idx_1 = 0; idx_1 < test_14; idx_1 = idx_1 + 1) {
- playWaveIndexed(0, pos, 48); // advance disabled do to parent repetition
- }
- pos = pos + 48;
-}"""
- self.assertEqual(expected, seqc_code)
-
-
-class UserRegisterTest(unittest.TestCase):
- def test_conversions(self):
- reg = UserRegister(zero_based_value=3)
- self.assertEqual(3, reg.to_seqc())
- self.assertEqual(3, reg.to_labone())
- self.assertEqual(4, reg.to_web_interface())
-
- reg = UserRegister(one_based_value=4)
- self.assertEqual(3, reg.to_seqc())
- self.assertEqual(3, reg.to_labone())
- self.assertEqual(4, reg.to_web_interface())
-
- self.assertEqual(reg, UserRegister.from_seqc(3))
- self.assertEqual(reg, UserRegister.from_labone(3))
- self.assertEqual(reg, UserRegister.from_web_interface(4))
-
- def test_formatting(self):
- reg = UserRegister.from_seqc(3)
-
- with self.assertRaises(ValueError):
- '{}'.format(reg)
-
- self.assertEqual('3', '{:seqc}'.format(reg))
- self.assertEqual('4', '{:web}'.format(reg))
- self.assertEqual('UserRegister(zero_based_value=3)', repr(reg))
- self.assertEqual(repr(reg), '{:r}'.format(reg))
-
-
-class UserRegisterManagerTest(unittest.TestCase):
- def test_require(self):
- manager = UserRegisterManager([7, 8, 9], 'test{register}')
-
- required = [manager.request(0), manager.request(1), manager.request(2)]
-
- self.assertEqual({'test7', 'test8', 'test9'}, set(required))
- self.assertEqual(required[1], manager.request(1))
-
- with self.assertRaisesRegex(ValueError, "No register"):
- manager.request(3)
-
-
-class HDAWGProgramManagerTest(unittest.TestCase):
- @unittest.skipIf(sys.version_info.minor < 6, "This test requires dict to be ordered.")
- def test_full_run(self):
- defined_channels = frozenset(['A', 'B', 'C'])
-
- unique_n = 1000
- unique_duration = 32
-
- unique_wfs = get_unique_wfs(n=unique_n, duration=unique_duration, defined_channels=defined_channels)
- same_wf = DummyWaveform(duration=48, sample_output=np.ones(48), defined_channels=defined_channels)
-
- channels = ('A', 'B')
- markers = ('C', None, 'A', None)
- amplitudes = (1., 1.)
- offsets = (0., 0.)
- volatage_transformations = (lambda x: x, lambda x: x)
- sample_rate = 1
-
- root = complex_program_as_loop(unique_wfs, wf_same=same_wf)
- seqc_nodes = complex_program_as_seqc(unique_wfs, wf_same=same_wf)
-
- manager = HDAWGProgramManager()
-
- manager.add_program('test', root, channels, markers, amplitudes, offsets, volatage_transformations, sample_rate)
-
- # 0: Program selection
- # 1: Trigger
- self.assertEqual({UserRegister(zero_based_value=2): 7}, manager.get_register_values('test'))
- seqc_program = manager.to_seqc_program()
- expected_program = """const PROG_SEL_REGISTER = 0;
-const TRIGGER_REGISTER = 1;
-const TRIGGER_RESET_MASK = 0b10000000000000000000000000000000;
-const PROG_SEL_NONE = 0;
-const NO_RESET_MASK = 0b10000000000000000000000000000000;
-const PLAYBACK_FINISHED_MASK = 0b1000000000000000000000000000000;
-const PROG_SEL_MASK = 0b111111111111111111111111111111;
-const INVERTED_PROG_SEL_MASK = 0b11000000000000000000000000000000;
-const IDLE_WAIT_CYCLES = 300;
-wave test_concatenated_waveform_0 = "c45d955d9dc472d46bf74f7eb9ae2ed4d159adea7d6fe9ce3f48c95423535333";
-wave test_shared_waveform_121f5c6e8822793b3836fb3098fa4591b91d4c205cc2d8afd01ee1bf6956e518 = "121f5c6e8822793b3836fb3098fa4591b91d4c205cc2d8afd01ee1bf6956e518";
-
-// function used by manually triggered programs
-void waitForSoftwareTrigger() {
- while (true) {
- var trigger_register = getUserReg(TRIGGER_REGISTER);
- if (trigger_register & TRIGGER_RESET_MASK) setUserReg(TRIGGER_REGISTER, 0);
- if (trigger_register) return;
- }
-}
-
-
-// program definitions
-void test_function() {
- var pos = 0;
- var user_reg_2 = getUserReg(2);
- waitForSoftwareTrigger();
- var init_pos_1 = pos;
- repeat(12) {
- pos = init_pos_1;
- repeat(1000) { // stepping repeat
- repeat(10) {
- repeat(42) {
- playWaveIndexed(test_concatenated_waveform_0, pos, 32); // advance disabled do to parent repetition
- }
- repeat(98) {
- playWave(test_shared_waveform_121f5c6e8822793b3836fb3098fa4591b91d4c205cc2d8afd01ee1bf6956e518);
- }
- }
- pos = pos + 32;
- }
- repeat(21) {
- playWaveIndexed(test_concatenated_waveform_0, pos, 32); // advance disabled do to parent repetition
- }
- pos = pos + 32;
- repeat(23) {
- playWaveIndexed(test_concatenated_waveform_0, pos, 48); // advance disabled do to parent repetition
- }
- pos = pos + 48;
- var idx_2;
- for(idx_2 = 0; idx_2 < user_reg_2; idx_2 = idx_2 + 1) {
- playWaveIndexed(test_concatenated_waveform_0, pos, 48); // advance disabled do to parent repetition
- }
- pos = pos + 48;
- }
-}
-
-// Declare and initialize global variables
-// Selected program index (0 -> None)
-var prog_sel = 0;
-
-// Value that gets written back to program selection register.
-// Used to signal that at least one program was played completely.
-var new_prog_sel = 0;
-
-// Is OR'ed to new_prog_sel.
-// Set to PLAYBACK_FINISHED_MASK if a program was played completely.
-var playback_finished = 0;
-
-
-// runtime block
-while (true) {
- // read program selection value
- prog_sel = getUserReg(PROG_SEL_REGISTER);
-
- // calculate value to write back to PROG_SEL_REGISTER
- new_prog_sel = prog_sel | playback_finished;
- if (!(prog_sel & NO_RESET_MASK)) new_prog_sel &= INVERTED_PROG_SEL_MASK;
- setUserReg(PROG_SEL_REGISTER, new_prog_sel);
-
- // reset playback flag
- playback_finished = 0;
-
- // only use part of prog sel that does not mean other things to select the program.
- prog_sel &= PROG_SEL_MASK;
-
- switch (prog_sel) {
- case 1:
- test_function();
- waitWave();
- playback_finished = PLAYBACK_FINISHED_MASK;
- default:
- wait(IDLE_WAIT_CYCLES);
- }
-}"""
- self.assertEqual(expected_program, seqc_program)
-
- @unittest.skipIf(sys.version_info.minor < 6, "This test requires dict to be ordered.")
- def test_full_run_with_dynamic_rate_reduction(self):
- defined_channels = frozenset(['A', 'B', 'C'])
-
- unique_n = 1000
- unique_duration = 192
-
- unique_wfs = get_constant_unique_wfs(n=unique_n, duration=unique_duration,
- defined_channels=defined_channels)
- same_wf = DummyWaveform(duration=48, sample_output=np.ones(48), defined_channels=defined_channels)
-
- channels = ('A', 'B')
- markers = ('C', None, 'A', None)
- amplitudes = (1., 1.)
- offsets = (0., 0.)
- volatage_transformations = (lambda x: x, lambda x: x)
- sample_rate = 1
-
- old_value, WaveformPlayback.ENABLE_DYNAMIC_RATE_REDUCTION = WaveformPlayback.ENABLE_DYNAMIC_RATE_REDUCTION, True
- try:
- root = complex_program_as_loop(unique_wfs, wf_same=same_wf)
- seqc_nodes = complex_program_as_seqc(unique_wfs, wf_same=same_wf)
-
- manager = HDAWGProgramManager()
-
- manager.add_program('test', root, channels, markers, amplitudes, offsets, volatage_transformations,
- sample_rate)
- finally:
- WaveformPlayback.ENABLE_DYNAMIC_RATE_REDUCTION = old_value
-
-
-
- # 0: Program selection
- # 1: Trigger
- self.assertEqual({UserRegister(zero_based_value=2): 7}, manager.get_register_values('test'))
- seqc_program = manager.to_seqc_program()
- expected_program = """const PROG_SEL_REGISTER = 0;
-const TRIGGER_REGISTER = 1;
-const TRIGGER_RESET_MASK = 0b10000000000000000000000000000000;
-const PROG_SEL_NONE = 0;
-const NO_RESET_MASK = 0b10000000000000000000000000000000;
-const PLAYBACK_FINISHED_MASK = 0b1000000000000000000000000000000;
-const PROG_SEL_MASK = 0b111111111111111111111111111111;
-const INVERTED_PROG_SEL_MASK = 0b11000000000000000000000000000000;
-const IDLE_WAIT_CYCLES = 300;
-wave test_concatenated_waveform_0 = "7fd412eb866ad371f717857ea33b309ec458c6c3469c7e51dcffcdce9a8c2679";
-wave test_shared_waveform_121f5c6e8822793b3836fb3098fa4591b91d4c205cc2d8afd01ee1bf6956e518 = "121f5c6e8822793b3836fb3098fa4591b91d4c205cc2d8afd01ee1bf6956e518";
-
-// function used by manually triggered programs
-void waitForSoftwareTrigger() {
- while (true) {
- var trigger_register = getUserReg(TRIGGER_REGISTER);
- if (trigger_register & TRIGGER_RESET_MASK) setUserReg(TRIGGER_REGISTER, 0);
- if (trigger_register) return;
- }
-}
-
-
-// program definitions
-void test_function() {
- var pos = 0;
- var user_reg_2 = getUserReg(2);
- waitForSoftwareTrigger();
- var init_pos_1 = pos;
- repeat(12) {
- pos = init_pos_1;
- repeat(1000) { // stepping repeat
- repeat(10) {
- repeat(42) {
- playWaveIndexed(test_concatenated_waveform_0, pos, 48, 2); // advance disabled do to parent repetition
- }
- repeat(98) {
- playWave(test_shared_waveform_121f5c6e8822793b3836fb3098fa4591b91d4c205cc2d8afd01ee1bf6956e518, 0);
- }
- }
- pos = pos + 48;
- }
- repeat(21) {
- playWaveIndexed(test_concatenated_waveform_0, pos, 48, 2); // advance disabled do to parent repetition
- }
- pos = pos + 48;
- repeat(23) {
- playWaveIndexed(test_concatenated_waveform_0, pos, 48, 0); // advance disabled do to parent repetition
- }
- pos = pos + 48;
- var idx_2;
- for(idx_2 = 0; idx_2 < user_reg_2; idx_2 = idx_2 + 1) {
- playWaveIndexed(test_concatenated_waveform_0, pos, 48, 0); // advance disabled do to parent repetition
- }
- pos = pos + 48;
- }
-}
-
-// Declare and initialize global variables
-// Selected program index (0 -> None)
-var prog_sel = 0;
-
-// Value that gets written back to program selection register.
-// Used to signal that at least one program was played completely.
-var new_prog_sel = 0;
-
-// Is OR'ed to new_prog_sel.
-// Set to PLAYBACK_FINISHED_MASK if a program was played completely.
-var playback_finished = 0;
-
-
-// runtime block
-while (true) {
- // read program selection value
- prog_sel = getUserReg(PROG_SEL_REGISTER);
-
- // calculate value to write back to PROG_SEL_REGISTER
- new_prog_sel = prog_sel | playback_finished;
- if (!(prog_sel & NO_RESET_MASK)) new_prog_sel &= INVERTED_PROG_SEL_MASK;
- setUserReg(PROG_SEL_REGISTER, new_prog_sel);
-
- // reset playback flag
- playback_finished = 0;
-
- // only use part of prog sel that does not mean other things to select the program.
- prog_sel &= PROG_SEL_MASK;
-
- switch (prog_sel) {
- case 1:
- test_function();
- waitWave();
- playback_finished = PLAYBACK_FINISHED_MASK;
- default:
- wait(IDLE_WAIT_CYCLES);
- }
-}"""
- self.assertEqual(expected_program, seqc_program)
\ No newline at end of file
diff --git a/tests/_program/tabor_tests.py b/tests/_program/tabor_tests.py
index ab8cddd30..bb1b7386b 100644
--- a/tests/_program/tabor_tests.py
+++ b/tests/_program/tabor_tests.py
@@ -1,6 +1,7 @@
import unittest
import itertools
import numpy as np
+from copy import deepcopy
from qupulse.utils.types import FrozenDict
from unittest import mock
@@ -15,9 +16,10 @@
except ImportError:
pytabor = None
-from qupulse._program.tabor import TaborException, TaborProgram, \
+from qupulse._program.tabor import TaborException, TaborProgram, find_place_for_segments_in_memory,\
TaborSegment, TaborSequencing, PlottableProgram, TableDescription, make_combined_wave, TableEntry
-from qupulse._program._loop import Loop
+from qupulse.program.loop import Loop
+from qupulse.program.waveforms import ConstantWaveform, SubsetWaveform
from qupulse._program.volatile import VolatileRepetitionCount
from qupulse.hardware.util import voltage_to_uint16
from qupulse.utils.types import TimeType
@@ -231,8 +233,8 @@ def test_depth_1_single_waveform(self):
self.assertEqual(t_program.get_advanced_sequencer_table(), [TableDescription(1, 1, 0)])
def test_depth_1_single_sequence(self):
- program = Loop(children=[Loop(waveform=DummyWaveform(defined_channels={'A'}, duration=1), repetition_count=3),
- Loop(waveform=DummyWaveform(defined_channels={'A'}, duration=1), repetition_count=4)],
+ program = Loop(children=[Loop(waveform=DummyWaveform(sample_output={'A': 0.1}, duration=1), repetition_count=3),
+ Loop(waveform=DummyWaveform(sample_output={'A': 0.2}, duration=1), repetition_count=4)],
repetition_count=1)
t_program = TaborProgram(program, channels=(None, 'A'), markers=(None, None),
@@ -246,8 +248,8 @@ def test_depth_1_single_sequence(self):
def test_depth_1_single_sequence_2(self):
"""Use the same wf twice"""
- wf_1 = DummyWaveform(defined_channels={'A'}, duration=1)
- wf_2 = DummyWaveform(defined_channels={'A'}, duration=1)
+ wf_1 = DummyWaveform(sample_output={'A': 0.1}, duration=1)
+ wf_2 = DummyWaveform(sample_output={'A': 0.2}, duration=1)
program = Loop(children=[Loop(waveform=wf_1, repetition_count=3),
Loop(waveform=wf_2, repetition_count=4),
@@ -265,8 +267,8 @@ def test_depth_1_single_sequence_2(self):
self.assertEqual(t_program.get_advanced_sequencer_table(), [TableDescription(1, 1, 0)])
def test_depth_1_advanced_sequence_unroll(self):
- wf_1 = DummyWaveform(defined_channels={'A'}, duration=1)
- wf_2 = DummyWaveform(defined_channels={'A'}, duration=1)
+ wf_1 = DummyWaveform(sample_output={'A': 0.1}, duration=1)
+ wf_2 = DummyWaveform(sample_output={'A': 0.2}, duration=1)
program = Loop(children=[Loop(waveform=wf_1, repetition_count=3),
Loop(waveform=wf_2, repetition_count=4)],
@@ -284,8 +286,8 @@ def test_depth_1_advanced_sequence_unroll(self):
self.assertEqual(t_program.get_advanced_sequencer_table(), [TableEntry(5, 1, 0)])
def test_depth_1_advanced_sequence(self):
- wf_1 = DummyWaveform(defined_channels={'A'}, duration=1)
- wf_2 = DummyWaveform(defined_channels={'A'}, duration=1)
+ wf_1 = DummyWaveform(sample_output={'A': 0.1}, duration=1)
+ wf_2 = DummyWaveform(sample_output={'A': 0.2}, duration=1)
program = Loop(children=[Loop(waveform=wf_1, repetition_count=3),
Loop(waveform=wf_2, repetition_count=4),
@@ -379,13 +381,31 @@ def my_gen(gen):
np.testing.assert_equal(sampled_seg.ch_a, data[0])
np.testing.assert_equal(sampled_seg.ch_b, data[1])
+ def test_calc_sampled_segments_deduplication(self):
+ wf1 = ConstantWaveform(duration=2, amplitude=0.1, channel='A')
+ wf2 = SubsetWaveform(
+ ConstantWaveform.from_mapping(duration=2, constant_values={'A': 0.1, 'B': 0.2}),
+ {'A'}
+ )
+ wf3 = ConstantWaveform(duration=1, amplitude=0.2, channel='A')
+
+ loop = Loop(children=[
+ Loop(waveform=wf1),
+ Loop(waveform=wf2),
+ Loop(waveform=wf3),
+ ])
+ prog = TaborProgram(loop, self.instr_props, ('A', None), (None, None), **self.program_entry_kwargs)
+ sampled, sampled_length = prog.get_sampled_segments()
+ self.assertEqual(len(sampled), 2)
+ self.assertEqual([192 * 2, 192], list(sampled_length))
+
def test_update_volatile_parameters_with_depth1(self):
parameters = {'s': 10, 'not': 13}
s = VolatileRepetitionCount(expression=ExpressionScalar('s'), scope=DictScope(values=FrozenDict(s=3),
volatile=set('s')))
- wf_1 = DummyWaveform(defined_channels={'A'}, duration=1)
- wf_2 = DummyWaveform(defined_channels={'A'}, duration=1)
+ wf_1 = DummyWaveform(sample_output={'A': 0.1}, duration=1)
+ wf_2 = DummyWaveform(sample_output={'A': 0.2}, duration=1)
program = Loop(children=[Loop(waveform=wf_1, repetition_count=s),
Loop(waveform=wf_2, repetition_count=4),
@@ -418,8 +438,8 @@ def test_update_volatile_parameters_with_depth2(self):
a = VolatileRepetitionCount(expression=ExpressionScalar('a'),
scope=DictScope(values=FrozenDict(a=5), volatile=set('a')))
- wf_1 = DummyWaveform(defined_channels={'A'}, duration=1)
- wf_2 = DummyWaveform(defined_channels={'A'}, duration=1)
+ wf_1 = DummyWaveform(sample_output={'A': 0.1}, duration=1)
+ wf_2 = DummyWaveform(sample_output={'A': 0.2}, duration=1)
program = Loop(children=[Loop(children=[Loop(waveform=wf_1, repetition_count=s),
Loop(waveform=wf_2, repetition_count=4),
@@ -704,3 +724,89 @@ def exec_general(self, data_1, data_2, fill_value=None):
with self.assertRaises(ValueError):
make_combined_wave(tabor_segments, destination_array=np.ones(16))
+
+
+class TaborMemoryManagementTests(unittest.TestCase):
+ def test_find_place_for_segments_in_memory(self):
+ # empty
+ kwargs = dict(
+ total_capacity=2**20,
+ current_segment_capacities=np.asarray([], dtype=np.uint32),
+ current_segment_hashes=np.asarray([], dtype=np.int64),
+ current_segment_references=np.asarray([], dtype=np.int32),
+ )
+ prev_kwargs = deepcopy(kwargs)
+
+ segments = np.asarray([-5, -6, -7, -8, -9])
+ segment_lengths = 192 + np.asarray([32, 16, 64, 32, 16])
+
+ w2s, ta, ti = find_place_for_segments_in_memory(
+ **kwargs,
+ new_segment_hashes=segments, new_segment_lengths=segment_lengths)
+ self.assertEqual(w2s.tolist(), [-1, -1, -1, -1, -1])
+ self.assertEqual(ta.tolist(), [True, True, True, True, True])
+ self.assertEqual(ti.tolist(), [-1, -1, -1, -1, -1])
+ np.testing.assert_equal(kwargs, prev_kwargs)
+
+ # all new segments
+ kwargs['current_segment_capacities'] = 192 + np.asarray([0, 16, 32, 16, 0], dtype=np.uint32)
+ kwargs['current_segment_hashes'] = np.asarray([1, 2, 3, 4, 5], dtype=np.int64)
+ kwargs['current_segment_references'] = np.asarray([1, 1, 1, 2, 1], dtype=np.int32)
+ prev_kwargs = deepcopy(kwargs)
+
+ w2s, ta, ti = find_place_for_segments_in_memory(new_segment_hashes=segments, new_segment_lengths=segment_lengths, **kwargs)
+ self.assertEqual(w2s.tolist(), [-1, -1, -1, -1, -1])
+ self.assertEqual(ta.tolist(), [True, True, True, True, True])
+ self.assertEqual(ti.tolist(), [-1, -1, -1, -1, -1])
+ np.testing.assert_equal(kwargs, prev_kwargs)
+
+ # some known segments
+ kwargs['current_segment_capacities'] = 192 + np.asarray([0, 16, 32, 64, 0, 16], dtype=np.uint32)
+ kwargs['current_segment_hashes'] = np.asarray([1, 2, 3, -7, 5, -9], dtype=np.int64)
+ kwargs['current_segment_references'] = np.asarray([1, 1, 1, 2, 1, 3], dtype=np.int32)
+ prev_kwargs = deepcopy(kwargs)
+
+ w2s, ta, ti = find_place_for_segments_in_memory(new_segment_hashes=segments, new_segment_lengths=segment_lengths, **kwargs)
+ self.assertEqual(w2s.tolist(), [-1, -1, 3, -1, 5])
+ self.assertEqual(ta.tolist(), [True, True, False, True, False])
+ self.assertEqual(ti.tolist(), [-1, -1, -1, -1, -1])
+ np.testing.assert_equal(kwargs, prev_kwargs)
+
+ # insert some segments with same length
+ kwargs['current_segment_capacities'] = 192 + np.asarray([0, 16, 32, 64, 0, 16], dtype=np.uint32)
+ kwargs['current_segment_hashes'] = np.asarray([1, 2, 3, 4, 5, 6], dtype=np.int64)
+ kwargs['current_segment_references'] = np.asarray([1, 0, 1, 0, 1, 3], dtype=np.int32)
+ prev_kwargs = deepcopy(kwargs)
+
+ w2s, ta, ti = find_place_for_segments_in_memory(new_segment_hashes=segments, new_segment_lengths=segment_lengths, **kwargs)
+ self.assertEqual(w2s.tolist(), [-1, -1, -1, -1, -1])
+ self.assertEqual(ta.tolist(), [True, False, False, True, True])
+ self.assertEqual(ti.tolist(), [-1, 1, 3, -1, -1])
+ np.testing.assert_equal(kwargs, prev_kwargs)
+
+ # insert some segments with smaller length
+ kwargs['current_segment_capacities'] = 192 + np.asarray([0, 80, 32, 64, 96, 16], dtype=np.uint32)
+ kwargs['current_segment_hashes'] = np.asarray([1, 2, 3, 4, 5, 6], dtype=np.int64)
+ kwargs['current_segment_references'] = np.asarray([1, 0, 1, 1, 0, 3], dtype=np.int32)
+ prev_kwargs = deepcopy(kwargs)
+
+ w2s, ta, ti = find_place_for_segments_in_memory(new_segment_hashes=segments, new_segment_lengths=segment_lengths, **kwargs)
+ self.assertEqual(w2s.tolist(), [-1, -1, -1, -1, -1])
+ self.assertEqual(ta.tolist(), [True, True, False, False, True])
+ self.assertEqual(ti.tolist(), [-1, -1, 4, 1, -1])
+ np.testing.assert_equal(kwargs, prev_kwargs)
+
+ # mix everything
+ segments = np.asarray([-5, -6, -7, -8, -9, -10, -11])
+ segment_lengths = 192 + np.asarray([32, 16, 64, 32, 16, 0, 0])
+
+ kwargs['current_segment_capacities'] = 192 + np.asarray([0, 80, 32, 64, 32, 16], dtype=np.uint32)
+ kwargs['current_segment_hashes'] = np.asarray([1, 2, 3, 4, -8, 6], dtype=np.int64)
+ kwargs['current_segment_references'] = np.asarray([1, 0, 1, 0, 1, 0], dtype=np.int32)
+ prev_kwargs = deepcopy(kwargs)
+
+ w2s, ta, ti = find_place_for_segments_in_memory(new_segment_hashes=segments, new_segment_lengths=segment_lengths, **kwargs)
+ self.assertEqual(w2s.tolist(), [-1, -1, -1, 4, -1, -1, -1])
+ self.assertEqual(ta.tolist(), [False, True, False, False, True, True, True])
+ self.assertEqual(ti.tolist(), [1, -1, 3, -1, -1, -1, -1])
+ np.testing.assert_equal(kwargs, prev_kwargs)
diff --git a/tests/_program/transformation_tests.py b/tests/_program/transformation_tests.py
index 3f3664822..23e0df90c 100644
--- a/tests/_program/transformation_tests.py
+++ b/tests/_program/transformation_tests.py
@@ -5,9 +5,10 @@
from qupulse.expressions import ExpressionScalar
-from qupulse._program.transformation import LinearTransformation, Transformation, IdentityTransformation,\
+from qupulse.program.transformation import LinearTransformation, Transformation, IdentityTransformation,\
ChainedTransformation, ParallelChannelTransformation, chain_transformations, OffsetTransformation,\
ScalingTransformation
+from qupulse.program.values import DynamicLinearValue
class TransformationStub(Transformation):
@@ -43,7 +44,7 @@ def test_chain(self):
self.assertIs(trafo.chain(IdentityTransformation()), trafo)
- with mock.patch('qupulse._program.transformation.chain_transformations',
+ with mock.patch('qupulse.program.transformation.chain_transformations',
return_value='asd') as chain_transformations:
self.assertEqual(trafo.chain(trafo), 'asd')
chain_transformations.assert_called_once_with(trafo, trafo)
@@ -64,10 +65,12 @@ def test_compare_key_and_init(self):
matrix_2 = np.array([[1, 1, 1], [1, 0, -1]])
trafo_2 = LinearTransformation(matrix_2, in_chs_2, out_chs_2)
- self.assertEqual(trafo.compare_key, trafo_2.compare_key)
+ with self.assertWarns(DeprecationWarning):
+ self.assertEqual(trafo.compare_key, trafo_2.compare_key)
self.assertEqual(trafo, trafo_2)
self.assertEqual(hash(trafo), hash(trafo_2))
- self.assertEqual(trafo.compare_key, (in_chs, out_chs, matrix.tobytes()))
+ with self.assertWarns(DeprecationWarning):
+ self.assertEqual(trafo.compare_key, (in_chs, out_chs, matrix.tobytes()))
def test_from_pandas(self):
try:
@@ -127,7 +130,7 @@ def test_call(self):
data['ignored'] = np.arange(116., 120.)
- transformed = trafo(np.full(4, np.NaN), data)
+ transformed = trafo(np.full(4, np.nan), data)
expected = {'transformed_a': data['a'] - data['b'],
'transformed_b': np.sum(raw_data, axis=0),
@@ -138,7 +141,7 @@ def test_call(self):
data.pop('c')
with self.assertRaisesRegex(KeyError, 'Invalid input channels'):
- trafo(np.full(4, np.NaN), data)
+ trafo(np.full(4, np.nan), data)
in_chs = ('a', 'b', 'c')
out_chs = ('a', 'b', 'c')
@@ -146,7 +149,7 @@ def test_call(self):
trafo = LinearTransformation(matrix, in_chs, out_chs)
data_in = {'ignored': np.arange(116., 120.)}
- transformed = trafo(np.full(4, np.NaN), data_in)
+ transformed = trafo(np.full(4, np.nan), data_in)
np.testing.assert_equal(transformed, data_in)
self.assertIs(data_in['ignored'], transformed['ignored'])
@@ -175,8 +178,13 @@ def test_constant_propagation(self):
class IdentityTransformationTests(unittest.TestCase):
def test_compare_key(self):
- self.assertIsNone(IdentityTransformation().compare_key)
-
+ with self.assertWarns(DeprecationWarning):
+ self.assertIsNone(IdentityTransformation().compare_key)
+
+ def test_sweepval(self):
+ with self.assertRaises(NotImplementedError):
+ IdentityTransformation().contains_dynamic_value()
+
def test_singleton(self):
self.assertIs(IdentityTransformation(), IdentityTransformation())
@@ -216,7 +224,8 @@ def test_init_and_properties(self):
chained = ChainedTransformation(*trafos)
self.assertEqual(chained.transformations, trafos)
- self.assertIs(chained.transformations, chained.compare_key)
+ with self.assertWarns(DeprecationWarning):
+ self.assertIs(chained.transformations, chained.compare_key)
def test_get_output_channels(self):
trafos = TransformationStub(), TransformationStub(), TransformationStub()
@@ -277,7 +286,7 @@ def test_chain(self):
trafo = TransformationStub()
chained = ChainedTransformation(*trafos)
- with mock.patch('qupulse._program.transformation.chain_transformations',
+ with mock.patch('qupulse.program.transformation.chain_transformations',
return_value='asd') as chain_transformations:
self.assertEqual(chained.chain(trafo), 'asd')
chain_transformations.assert_called_once_with(*trafos, trafo)
@@ -485,6 +494,17 @@ def test_time_dependence(self):
}, transformed)
+ def test_sweepval(self):
+ channels = {'X': 2, 'Y': DynamicLinearValue(0.1, {'a':0.02})}
+ trafo = OffsetTransformation(channels)
+ self.assertEqual(trafo.contains_dynamic_value(), True)
+
+ channels = {'X': 2, 'Y': 2}
+ trafo = OffsetTransformation(channels)
+ self.assertEqual(trafo.contains_dynamic_value(), False)
+
+
+
class TestScalingTransformation(unittest.TestCase):
def setUp(self) -> None:
self.constant_scales = {'A': 1.5, 'B': 1.2}
@@ -557,3 +577,12 @@ def test_time_dependence(self):
'Z': np.tan(t) * np.exp(t),
'K': values['K']
}, transformed)
+
+ def test_sweepval(self):
+ channels = {'X': 2, 'Y': DynamicLinearValue(0.1, {'a':0.02})}
+ trafo = ScalingTransformation(channels)
+ self.assertEqual(trafo.contains_dynamic_value(), True)
+
+ channels = {'X': 2, 'Y': 2}
+ trafo = ScalingTransformation(channels)
+ self.assertEqual(trafo.contains_dynamic_value(), False)
diff --git a/tests/_program/waveforms_tests.py b/tests/_program/waveforms_tests.py
index d84b2c763..ece22dce1 100644
--- a/tests/_program/waveforms_tests.py
+++ b/tests/_program/waveforms_tests.py
@@ -7,10 +7,10 @@
from qupulse.utils.types import TimeType
from qupulse.pulses.interpolation import HoldInterpolationStrategy, LinearInterpolationStrategy,\
JumpInterpolationStrategy
-from qupulse._program.waveforms import MultiChannelWaveform, RepetitionWaveform, SequenceWaveform,\
+from qupulse.program.waveforms import MultiChannelWaveform, RepetitionWaveform, SequenceWaveform,\
TableWaveformEntry, TableWaveform, TransformingWaveform, SubsetWaveform, ArithmeticWaveform, ConstantWaveform,\
Waveform, FunctorWaveform, FunctionWaveform, ReversedWaveform
-from qupulse._program.transformation import LinearTransformation
+from qupulse.program.transformation import LinearTransformation
from qupulse.expressions import ExpressionScalar, Expression
from tests.pulses.sequencing_dummies import DummyWaveform, DummyInterpolationStrategy
@@ -207,7 +207,10 @@ def test_init_several_channels(self) -> None:
dwf_c_valid = DummyWaveform(duration=2.2, defined_channels={'C'})
waveform_flat = MultiChannelWaveform.from_parallel((waveform, dwf_c_valid))
- self.assertEqual(len(waveform_flat.compare_key), 3)
+ self.assertEqual(
+ MultiChannelWaveform([dwf_a, dwf_b, dwf_c_valid]),
+ waveform_flat
+ )
def test_unsafe_sample(self) -> None:
sample_times = numpy.linspace(98.5, 103.5, num=11)
@@ -330,10 +333,17 @@ def test_defined_channels(self):
body_wf = DummyWaveform(defined_channels={'a'})
self.assertIs(RepetitionWaveform(body_wf, 2).defined_channels, body_wf.defined_channels)
- def test_compare_key(self):
- body_wf = DummyWaveform(defined_channels={'a'})
- wf = RepetitionWaveform(body_wf, 2)
- self.assertEqual(wf.compare_key, (body_wf.compare_key, 2))
+ def test_equality(self):
+ body_wf_1 = DummyWaveform(defined_channels={'a'})
+ wf_1 = RepetitionWaveform(body_wf_1, 2)
+ body_wf_2 = DummyWaveform(defined_channels={'a'})
+ wf_2 = RepetitionWaveform(body_wf_2, 2)
+ wf_3 = RepetitionWaveform(body_wf_1, 3)
+ wf_1_equal = RepetitionWaveform(body_wf_1, 2)
+ self.assertEqual(wf_1_equal, wf_1)
+ self.assertNotEqual(wf_1, wf_2)
+ self.assertNotEqual(wf_1, wf_3)
+ self.assertEqual({wf_1, wf_2, wf_3}, {wf_1, wf_2, wf_3, wf_1_equal})
def test_unsafe_get_subset_for_channels(self):
body_wf = DummyWaveform(defined_channels={'a', 'b'})
@@ -395,12 +405,11 @@ def test_init(self):
swf1 = SequenceWaveform((dwf_ab, dwf_ab))
self.assertEqual(swf1.duration, 2*dwf_ab.duration)
- self.assertEqual(len(swf1.compare_key), 2)
+ self.assertEqual(swf1.sequenced_waveforms, (dwf_ab, dwf_ab))
swf2 = SequenceWaveform((swf1, dwf_ab))
self.assertEqual(swf2.duration, 3 * dwf_ab.duration)
-
- self.assertEqual(len(swf2.compare_key), 2)
+ self.assertEqual(swf2.sequenced_waveforms, (swf1, dwf_ab))
def test_from_sequence(self):
dwf = DummyWaveform(duration=1.1, defined_channels={'A'})
@@ -478,12 +487,12 @@ def test_unsafe_get_subset_for_channels(self):
sub_wf = wf.unsafe_get_subset_for_channels(subset)
self.assertIsInstance(sub_wf, SequenceWaveform)
- self.assertEqual(len(sub_wf.compare_key), 2)
- self.assertEqual(sub_wf.compare_key[0].defined_channels, subset)
- self.assertEqual(sub_wf.compare_key[1].defined_channels, subset)
+ self.assertEqual(len(sub_wf.sequenced_waveforms), 2)
+ self.assertEqual(sub_wf.sequenced_waveforms[0].defined_channels, subset)
+ self.assertEqual(sub_wf.sequenced_waveforms[1].defined_channels, subset)
- self.assertEqual(sub_wf.compare_key[0].duration, TimeType.from_float(2.2))
- self.assertEqual(sub_wf.compare_key[1].duration, TimeType.from_float(3.3))
+ self.assertEqual(sub_wf.sequenced_waveforms[0].duration, TimeType.from_float(2.2))
+ self.assertEqual(sub_wf.sequenced_waveforms[1].duration, TimeType.from_float(3.3))
def test_repr(self):
cwf_2_a = ConstantWaveform(duration=1.1, amplitude=2.2, channel='A')
@@ -714,7 +723,8 @@ def test_simple_properties(self):
self.assertIs(trafo_wf.inner_waveform, inner_wf)
self.assertIs(trafo_wf.transformation, trafo)
- self.assertEqual(trafo_wf.compare_key, (inner_wf, trafo))
+ with self.assertWarns(DeprecationWarning):
+ self.assertEqual(trafo_wf.compare_key, (inner_wf, trafo))
self.assertIs(trafo_wf.duration, inner_wf.duration)
self.assertIs(trafo_wf.defined_channels, output_channels)
trafo.get_output_channels.assert_called_once_with(inner_wf.defined_channels)
@@ -804,7 +814,8 @@ def test_simple_properties(self):
subset_wf = SubsetWaveform(inner_wf, {'a', 'c'})
self.assertIs(subset_wf.inner_waveform, inner_wf)
- self.assertEqual(subset_wf.compare_key, (frozenset(['a', 'c']), inner_wf))
+ with self.assertWarns(DeprecationWarning):
+ self.assertEqual(subset_wf.compare_key, (frozenset(['a', 'c']), inner_wf))
self.assertIs(subset_wf.duration, inner_wf.duration)
self.assertEqual(subset_wf.defined_channels, {'a', 'c'})
@@ -891,8 +902,8 @@ def test_simple_properties(self):
self.assertIs(rhs, arith.rhs)
self.assertEqual('-', arith.arithmetic_operator)
self.assertEqual(lhs.duration, arith.duration)
-
- self.assertEqual(('-', lhs, rhs), arith.compare_key)
+ with self.assertWarns(DeprecationWarning):
+ self.assertEqual(('-', lhs, rhs), arith.compare_key)
def test_unsafe_get_subset_for_channels(self):
lhs = DummyWaveform(duration=1.5, defined_channels={'a', 'b', 'c'})
@@ -944,10 +955,12 @@ def test_equality(self) -> None:
wf1b = FunctionWaveform(ExpressionScalar('2*t'), 3, channel='A')
wf3 = FunctionWaveform(ExpressionScalar('2*t+2'), 3, channel='A')
wf4 = FunctionWaveform(ExpressionScalar('2*t'), 4, channel='A')
+ wf5 = FunctionWaveform(ExpressionScalar('2*t'), 3, channel='B')
self.assertEqual(wf1a, wf1a)
self.assertEqual(wf1a, wf1b)
self.assertNotEqual(wf1a, wf3)
self.assertNotEqual(wf1a, wf4)
+ self.assertNotEqual(wf1a, wf5)
def test_defined_channels(self) -> None:
wf = FunctionWaveform(ExpressionScalar('t'), 4, channel='A')
@@ -1056,7 +1069,7 @@ def test_unsafe_get_subset_for_channels(self):
wf.unsafe_get_subset_for_channels({'A'}))
inner_subset.assert_called_once_with({'A'})
- def test_compare_key(self):
+ def test_comparison(self):
inner_wf_1 = DummyWaveform(defined_channels={'A', 'B'})
inner_wf_2 = DummyWaveform(defined_channels={'A', 'B'})
functors_1 = dict(A=np.positive, B=np.negative)
@@ -1067,7 +1080,8 @@ def test_compare_key(self):
wf21 = FunctorWaveform(inner_wf_2, functors_1)
wf22 = FunctorWaveform(inner_wf_2, functors_2)
- self.assertEqual((inner_wf_1, frozenset(functors_1.items())), wf11.compare_key)
+ with self.assertWarns(DeprecationWarning):
+ self.assertEqual((inner_wf_1, frozenset(functors_1.items())), wf11.compare_key)
self.assertEqual(wf11, wf11)
self.assertEqual(wf11, FunctorWaveform(inner_wf_1, functors_1))
@@ -1083,7 +1097,8 @@ def test_simple_properties(self):
self.assertEqual(dummy_wf.duration, reversed_wf.duration)
self.assertEqual(dummy_wf.defined_channels, reversed_wf.defined_channels)
- self.assertEqual(dummy_wf.compare_key, reversed_wf.compare_key)
+ with self.assertWarns(DeprecationWarning):
+ self.assertEqual(dummy_wf.compare_key, reversed_wf.compare_key)
self.assertNotEqual(reversed_wf, dummy_wf)
def test_reversed_sample(self):
diff --git a/tests/backward_compatibility/hardware_test_helper.py b/tests/backward_compatibility/hardware_test_helper.py
index 1314283c2..2f2cef447 100644
--- a/tests/backward_compatibility/hardware_test_helper.py
+++ b/tests/backward_compatibility/hardware_test_helper.py
@@ -4,6 +4,7 @@
import typing
import importlib.util
import sys
+import warnings
from qupulse.serialization import Serializer, FilesystemBackend, PulseStorage
from qupulse.pulses.pulse_template import PulseTemplate
diff --git a/tests/backward_compatibility/tabor_backward_compatibility_tests.py b/tests/backward_compatibility/tabor_backward_compatibility_tests.py
index 6e44ab796..7b635283e 100644
--- a/tests/backward_compatibility/tabor_backward_compatibility_tests.py
+++ b/tests/backward_compatibility/tabor_backward_compatibility_tests.py
@@ -7,11 +7,9 @@
import warnings
try:
- import tabor_control
-except ImportError as err:
- raise unittest.SkipTest("tabor_control not present") from err
-
-from tests.hardware.tabor_simulator_based_tests import TaborSimulatorManager
+ from tests.hardware.tabor_simulator_based_tests import TaborSimulatorManager
+except ImportError:
+ TaborSimulatorManager = None
from tests.hardware.dummy_devices import DummyDAC
from tests.backward_compatibility.hardware_test_helper import LoadingAndSequencingHelper
@@ -102,6 +100,7 @@ def read_program(self):
return self.program_AB, self.program_CD
+@unittest.skipIf(tabor_control is None, "tabor_control not available")
class CompleteIntegrationTestHelper(unittest.TestCase):
data_folder = None
pulse_name = None
diff --git a/tests/backward_compatibility/zhinst_charge_scan_test.py b/tests/backward_compatibility/zhinst_charge_scan_tests.py
similarity index 98%
rename from tests/backward_compatibility/zhinst_charge_scan_test.py
rename to tests/backward_compatibility/zhinst_charge_scan_tests.py
index 93c0c8038..de932f8b1 100644
--- a/tests/backward_compatibility/zhinst_charge_scan_test.py
+++ b/tests/backward_compatibility/zhinst_charge_scan_tests.py
@@ -100,7 +100,8 @@ def setUpClass(cls):
cls.test_state = HDAWGLoadingAndSequencingHelper(cls.data_folder, cls.pulse_name)
def test_1_1_deserialization(self):
- self.test_state.deserialize_pulse()
+ with self.assertWarns(DeprecationWarning):
+ self.test_state.deserialize_pulse()
def test_1_2_deserialization_2018(self) -> None:
self.test_state.deserialize_pulse_2018()
diff --git a/tests/comparable_tests.py b/tests/comparable_tests.py
deleted file mode 100644
index 0394c7b3a..000000000
--- a/tests/comparable_tests.py
+++ /dev/null
@@ -1,31 +0,0 @@
-import unittest
-from typing import Any
-
-from qupulse.comparable import Comparable
-
-class DummyComparable(Comparable):
-
- def __init__(self, compare_key: Any) -> None:
- super().__init__()
- self.compare_key_ = compare_key
-
- @property
- def compare_key(self) -> Any:
- return self.compare_key_
-
-
-class ComparableTests(unittest.TestCase):
-
- def test_hash(self) -> None:
- comp_a = DummyComparable(17)
- self.assertEqual(hash(17), hash(comp_a))
-
- def test_eq(self) -> None:
- comp_a = DummyComparable(17)
- comp_b = DummyComparable(18)
- comp_c = DummyComparable(18)
- self.assertNotEqual(comp_a, comp_b)
- self.assertNotEqual(comp_b, comp_a)
- self.assertEqual(comp_b, comp_c)
- self.assertNotEqual(comp_a, "foo")
- self.assertNotEqual("foo", comp_a)
diff --git a/tests/expressions/__init__.py b/tests/expressions/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/expression_tests.py b/tests/expressions/expression_tests.py
similarity index 93%
rename from tests/expression_tests.py
rename to tests/expressions/expression_tests.py
index 26693821c..b11094ea0 100644
--- a/tests/expression_tests.py
+++ b/tests/expressions/expression_tests.py
@@ -1,464 +1,479 @@
-import pickle
-import unittest
-import sys
-
-import numpy as np
-import sympy.abc
-from sympy import sympify, Eq
-
-from qupulse.expressions import Expression, ExpressionVariableMissingException, NonNumericEvaluation, ExpressionScalar, ExpressionVector
-from qupulse.utils.types import TimeType
-
-class ExpressionTests(unittest.TestCase):
- def test_make(self):
- self.assertTrue(Expression.make('a') == 'a')
- self.assertTrue(Expression.make('a + b') == 'a + b')
- self.assertTrue(Expression.make(9) == 9)
-
- self.assertIsInstance(Expression.make([1, 'a']), ExpressionVector)
-
- self.assertIsInstance(ExpressionScalar.make('a'), ExpressionScalar)
- self.assertIsInstance(ExpressionVector.make(['a']), ExpressionVector)
-
-
-class ExpressionVectorTests(unittest.TestCase):
- def test_evaluate_numeric(self) -> None:
- e = ExpressionVector(['a * b + c', 'a + d'])
- params = {
- 'a': 2,
- 'b': 1.5,
- 'c': -7,
- 'd': 9
- }
- np.testing.assert_equal(np.array([2 * 1.5 - 7, 2 + 9]),
- e.evaluate_numeric(**params))
-
- with self.assertRaises(NonNumericEvaluation):
- params['a'] = sympify('h')
- e.evaluate_numeric(**params)
-
- def test_evaluate_numeric_2d(self) -> None:
- e = ExpressionVector([['a * b + c', 'a + d'], ['a', 3]])
- params = {
- 'a': 2,
- 'b': 1.5,
- 'c': -7,
- 'd': 9
- }
- np.testing.assert_equal(np.array([[2 * 1.5 - 7, 2 + 9], [2, 3]]),
- e.evaluate_numeric(**params))
-
- with self.assertRaises(NonNumericEvaluation):
- params['a'] = sympify('h')
- e.evaluate_numeric(**params)
-
- def test_partial_evaluation(self):
- e = ExpressionVector(['a * b + c', 'a + d'])
-
- params = {
- 'a': 2,
- 'b': 1.5,
- 'c': -7
- }
-
- expected = ExpressionVector([2 * 1.5 - 7, '2 + d'])
- evaluated = e.evaluate_symbolic(params)
-
- np.testing.assert_equal(evaluated.underlying_expression, expected.underlying_expression)
-
- def test_symbolic_evaluation(self):
- e = ExpressionVector([['a * b + c', 'a + d'], ['a', 3]])
- params = {
- 'a': 2,
- 'b': 1.5,
- 'c': -7,
- 'd': 9
- }
-
- expected = ExpressionVector([[2 * 1.5 - 7, 2 + 9], [2, 3]])
- evaluated = e.evaluate_symbolic(params)
-
- np.testing.assert_equal(evaluated.underlying_expression, expected.underlying_expression)
-
- def test_numeric_expression(self):
- numbers = np.linspace(1, 2, num=5)
-
- e = ExpressionVector(numbers)
-
- np.testing.assert_equal(e.underlying_expression, numbers)
-
- def test_eq(self):
- e1 = ExpressionVector([1, 2])
- e2 = ExpressionVector(['1', '2'])
- e3 = ExpressionVector(['1', 'a'])
- e4 = ExpressionVector([1, 'a'])
- e5 = ExpressionVector([1, 'a', 3])
- e6 = ExpressionVector([1, 1, '1'])
- e7 = ExpressionVector(['a'])
-
- self.assertEqual(e1, e2)
- self.assertEqual(e3, e4)
- self.assertNotEqual(e4, e5)
-
- self.assertEqual(e1, [1, 2])
- self.assertNotEqual(e6, 1)
- self.assertEqual(e7, ExpressionScalar('a'))
-
- def test_hash(self):
- e1 = ExpressionVector([1, 2])
- e2 = ExpressionVector(['1', '2'])
- e7 = ExpressionVector(['a'])
-
- s = ExpressionScalar('a')
- self.assertEqual({e1, e7}, {e1, e2, e7, s})
-
- def test_pickle(self):
- expr = ExpressionVector([1, 'a + 5', 3])
- # populate lambdified
- expr.evaluate_in_scope({'a': 3})
- dumped = pickle.dumps(expr)
- loaded = pickle.loads(dumped)
- self.assertEqual(expr, loaded)
-
-
-class ExpressionScalarTests(unittest.TestCase):
- def test_format(self):
- expr = ExpressionScalar('17')
- e_format = '{:.4e}'.format(expr)
- self.assertEqual(e_format, "1.7000e+01")
-
- empty_format = "{}".format(expr)
- self.assertEqual(empty_format, '17')
-
- expr_with_var = ExpressionScalar('17*a')
- with self.assertRaises(TypeError):
- # throw error on implicit float cast
- '{:.4e}'.format(expr_with_var)
-
- empty_format = "{}".format(expr_with_var)
- self.assertEqual(empty_format, '17*a')
-
- @unittest.skipIf(sys.version_info < (3, 6), "format string literals require 3.6 or higher")
- def test_fstring(self) -> None:
- src_code = """e = ExpressionScalar('2.0'); \
- self.assertEqual( f'{e}', str(e) ); \
- self.assertEqual( f'{e:.2f}', '%.2f' % e)
- """
- exec(src_code)
-
- def test_evaluate_numeric(self) -> None:
- e = ExpressionScalar('a * b + c')
- params = {
- 'a': 2,
- 'b': 1.5,
- 'c': -7
- }
- self.assertEqual(2 * 1.5 - 7, e.evaluate_numeric(**params))
-
- with self.assertRaises(NonNumericEvaluation):
- params['a'] = sympify('h')
- e.evaluate_numeric(**params)
-
- def test_evaluate_numpy(self):
- e = ExpressionScalar('a * b + c')
- params = {
- 'a': 2*np.ones(4),
- 'b': 1.5*np.ones(4),
- 'c': -7*np.ones(4)
- }
- np.testing.assert_equal((2 * 1.5 - 7) * np.ones(4), e.evaluate_numeric(**params))
-
- e = ExpressionScalar('a * b + c')
- params = {
- 'a': np.array(2),
- 'b': np.array(1.5),
- 'c': np.array(-7)
- }
- np.testing.assert_equal((2 * 1.5 - 7), e.evaluate_numeric(**params))
-
- def test_indexing(self):
- e = ExpressionScalar('a[i] * c')
-
- params = {
- 'a': np.array([1, 2, 3]),
- 'i': 1,
- 'c': 2
- }
-
- self.assertEqual(e.evaluate_numeric(**params), 2 * 2)
- params['a'] = [1, 2, 3]
- self.assertEqual(e.evaluate_numeric(**params), 2 * 2)
- params['a'] = np.array([[1, 2, 3], [4, 5, 6]])
- np.testing.assert_equal(e.evaluate_numeric(**params), 2 * np.array([4, 5, 6]))
-
- def test_partial_evaluation(self) -> None:
- e = ExpressionScalar('a * c')
- params = {'c': 5.5}
- evaluated = e.evaluate_symbolic(params)
- expected = ExpressionScalar('a * 5.5')
- self.assertEqual(expected.underlying_expression, evaluated.underlying_expression)
-
- def test_partial_evaluation_vectorized(self) -> None:
- e = ExpressionScalar('a[i] * c')
-
- params = {
- 'c': np.array([[1, 2], [3, 4]])
- }
-
- evaluated = e.evaluate_symbolic(params)
- expected = ExpressionVector([['a[i] * 1', 'a[i] * 2'], ['a[i] * 3', 'a[i] * 4']])
-
- np.testing.assert_equal(evaluated.underlying_expression, expected.underlying_expression)
-
- def test_evaluate_numeric_without_numpy(self):
- e = Expression('a * b + c')
-
- params = {
- 'a': 2,
- 'b': 1.5,
- 'c': -7
- }
- self.assertEqual(2 * 1.5 - 7, e.evaluate_numeric(**params))
-
- params = {
- 'a': 2j,
- 'b': 1.5,
- 'c': -7
- }
- self.assertEqual(2j * 1.5 - 7, e.evaluate_numeric(**params))
-
- params = {
- 'a': 2,
- 'b': 6,
- 'c': -7
- }
- self.assertEqual(2 * 6 - 7, e.evaluate_numeric(**params))
-
- params = {
- 'a': 2,
- 'b': sympify('k'),
- 'c': -7
- }
- with self.assertRaises(NonNumericEvaluation):
- e.evaluate_numeric(**params)
-
- def test_evaluate_symbolic(self):
- e = ExpressionScalar('a * b + c')
- params = {
- 'a': 'd',
- 'c': -7
- }
- result = e.evaluate_symbolic(params)
- expected = ExpressionScalar('d*b-7')
- self.assertEqual(result, expected)
-
- def test_variables(self) -> None:
- e = ExpressionScalar('4 ** pi + x * foo')
- expected = sorted(['foo', 'x'])
- received = sorted(e.variables)
- self.assertEqual(expected, received)
-
- def test_variables_indexed(self):
- e = ExpressionScalar('a[i] * c')
- expected = sorted(['a', 'i', 'c'])
- received = sorted(e.variables)
- self.assertEqual(expected, received)
-
- def test_evaluate_variable_missing(self) -> None:
- e = ExpressionScalar('a * b + c')
- params = {
- 'b': 1.5
- }
- with self.assertRaises(ExpressionVariableMissingException):
- e.evaluate_numeric(**params)
-
- def test_repr(self):
- s = 'a * b'
- e = ExpressionScalar(s)
- self.assertEqual("ExpressionScalar('a * b')", repr(e))
-
- def test_repr_original_expression_is_sympy(self):
- # in this case we test that we get the original expression back if we do
- # eval(repr(e))
-
- org = sympy.sympify(3.1415)
- e = ExpressionScalar(org)
- self.assertEqual(e, eval(repr(e)))
-
- org = sympy.abc.a * sympy.abc.b
- e = ExpressionScalar(org)
- self.assertEqual(e, eval(repr(e)))
-
- org = sympy.sympify('3/17')
- e = ExpressionScalar(org)
- self.assertEqual(e, eval(repr(e)))
-
- def test_str(self):
- s = 'a * b'
- e = ExpressionScalar(s)
- self.assertEqual('a*b', str(e))
-
- def test_original_expression(self):
- s = 'a * b'
- self.assertEqual(ExpressionScalar(s).original_expression, s)
-
- def test_hash(self):
- expected = {ExpressionScalar(2), ExpressionScalar('a')}
- sequence = [ExpressionScalar(2), ExpressionScalar('a'), ExpressionScalar(2), ExpressionScalar('a')]
- self.assertEqual(expected, set(sequence))
-
- def test_undefined_comparison(self):
- valued = ExpressionScalar(2)
- unknown = ExpressionScalar('a')
-
- self.assertIsNone(unknown < 0)
- self.assertIsNone(unknown > 0)
- self.assertIsNone(unknown >= 0)
- self.assertIsNone(unknown <= 0)
- self.assertFalse(unknown == 0)
-
- self.assertIsNone(0 < unknown)
- self.assertIsNone(0 > unknown)
- self.assertIsNone(0 <= unknown)
- self.assertIsNone(0 >= unknown)
- self.assertFalse(0 == unknown)
-
- self.assertIsNone(unknown < valued)
- self.assertIsNone(unknown > valued)
- self.assertIsNone(unknown >= valued)
- self.assertIsNone(unknown <= valued)
- self.assertFalse(unknown == valued)
-
- valued, unknown = unknown, valued
- self.assertIsNone(unknown < valued)
- self.assertIsNone(unknown > valued)
- self.assertIsNone(unknown >= valued)
- self.assertIsNone(unknown <= valued)
- self.assertFalse(unknown == valued)
- valued, unknown = unknown, valued
-
- self.assertFalse(unknown == valued)
-
- def test_defined_comparison(self):
- small = ExpressionScalar(2)
- large = ExpressionScalar(3)
-
- self.assertIs(small < small, False)
- self.assertIs(small > small, False)
- self.assertIs(small <= small, True)
- self.assertIs(small >= small, True)
- self.assertIs(small == small, True)
-
- self.assertIs(small < large, True)
- self.assertIs(small > large, False)
- self.assertIs(small <= large, True)
- self.assertIs(small >= large, False)
- self.assertIs(small == large, False)
-
- self.assertIs(large < small, False)
- self.assertIs(large > small, True)
- self.assertIs(large <= small, False)
- self.assertIs(large >= small, True)
- self.assertIs(large == small, False)
-
- def test_number_comparison(self):
- valued = ExpressionScalar(2)
-
- self.assertIs(valued < 3, True)
- self.assertIs(valued > 3, False)
- self.assertIs(valued <= 3, True)
- self.assertIs(valued >= 3, False)
-
- self.assertIs(valued == 3, False)
- self.assertIs(valued == 2, True)
- self.assertIs(3 == valued, False)
- self.assertIs(2 == valued, True)
-
- self.assertIs(3 < valued, False)
- self.assertIs(3 > valued, True)
- self.assertIs(3 <= valued, False)
- self.assertIs(3 >= valued, True)
-
- def assertExpressionEqual(self, lhs: Expression, rhs: Expression):
- self.assertTrue(bool(Eq(lhs, rhs)), '{} and {} are not equal'.format(lhs, rhs))
-
- def test_number_math(self):
- a = ExpressionScalar('a')
- b = 3.3
-
- self.assertExpressionEqual(a + b, b + a)
- self.assertExpressionEqual(a - b, -(b - a))
- self.assertExpressionEqual(a * b, b * a)
- self.assertExpressionEqual(a / b, 1 / (b / a))
-
- def test_symbolic_math(self):
- a = ExpressionScalar('a')
- b = ExpressionScalar('b')
-
- self.assertExpressionEqual(a + b, b + a)
- self.assertExpressionEqual(a - b, -(b - a))
- self.assertExpressionEqual(a * b, b * a)
- self.assertExpressionEqual(a / b, 1 / (b / a))
-
- def test_sympy_math(self):
- a = ExpressionScalar('a')
- b = sympify('b')
-
- self.assertExpressionEqual(a + b, b + a)
- self.assertExpressionEqual(a - b, -(b - a))
- self.assertExpressionEqual(a * b, b * a)
- self.assertExpressionEqual(a / b, 1 / (b / a))
-
- def test_is_nan(self):
- self.assertTrue(ExpressionScalar('nan').is_nan())
- self.assertTrue(ExpressionScalar('0./0.').is_nan())
-
- self.assertFalse(ExpressionScalar(456).is_nan())
-
- def test_special_function_numeric_evaluation(self):
- expr = Expression('erfc(t)')
- data = [-1., 0., 1.]
- expected = np.array([1.84270079, 1., 0.15729921])
- result = expr.evaluate_numeric(t=data)
-
- np.testing.assert_allclose(expected, result)
-
- def test_evaluate_with_exact_rationals(self):
- expr = ExpressionScalar('1 / 3')
- self.assertEqual(TimeType.from_fraction(1, 3), expr.evaluate_with_exact_rationals({}))
-
- expr = ExpressionScalar('a * (1 / 3)')
- self.assertEqual(TimeType.from_fraction(2, 3), expr.evaluate_with_exact_rationals({'a': 2}))
-
- expr = ExpressionScalar('dot(a, b) * (1 / 3)')
- self.assertEqual(TimeType.from_fraction(10, 3),
- expr.evaluate_with_exact_rationals({'a': [2, 2], 'b': [1, 4]}))
-
- def test_pickle(self):
- expr = ExpressionScalar('1 / a')
- # populate lambdified
- expr.evaluate_in_scope({'a': 7})
- dumped = pickle.dumps(expr)
- loaded = pickle.loads(dumped)
- self.assertEqual(expr, loaded)
-
-
-class ExpressionExceptionTests(unittest.TestCase):
- def test_expression_variable_missing(self):
- variable = 's'
- expression = ExpressionScalar('s*t')
-
- self.assertEqual(str(ExpressionVariableMissingException(variable, expression)),
- "Could not evaluate : A value for variable is missing!")
-
- def test_non_numeric_evaluation(self):
- expression = ExpressionScalar('a*b')
- call_arguments = dict()
-
- expected = "The result of evaluate_numeric is of type {} " \
- "which is not a number".format(float)
- self.assertEqual(str(NonNumericEvaluation(expression, 1., call_arguments)), expected)
-
- expected = "The result of evaluate_numeric is of type {} " \
- "which is not a number".format(np.zeros(1).dtype)
- self.assertEqual(str(NonNumericEvaluation(expression, np.zeros(1), call_arguments)), expected)
+import pickle
+import unittest
+import sys
+
+import numpy as np
+import sympy.abc
+from sympy import sympify, Eq
+
+from qupulse.expressions.sympy import Expression, ExpressionScalar, ExpressionVector
+from qupulse.expressions import ExpressionVariableMissingException, NonNumericEvaluation
+from qupulse.utils.types import TimeType
+
+class ExpressionTests(unittest.TestCase):
+ def test_make(self):
+ self.assertTrue(Expression.make('a') == 'a')
+ self.assertTrue(Expression.make('a + b') == 'a + b')
+ self.assertTrue(Expression.make(9) == 9)
+
+ self.assertIsInstance(Expression.make([1, 'a']), ExpressionVector)
+
+ self.assertIsInstance(ExpressionScalar.make('a'), ExpressionScalar)
+ self.assertIsInstance(ExpressionVector.make(['a']), ExpressionVector)
+
+
+class ExpressionVectorTests(unittest.TestCase):
+ def test_evaluate_numeric(self) -> None:
+ e = ExpressionVector(['a * b + c', 'a + d'])
+ params = {
+ 'a': 2,
+ 'b': 1.5,
+ 'c': -7,
+ 'd': 9
+ }
+ np.testing.assert_equal(np.array([2 * 1.5 - 7, 2 + 9]),
+ e.evaluate_numeric(**params))
+
+ with self.assertRaises(NonNumericEvaluation):
+ params['a'] = sympify('h')
+ e.evaluate_numeric(**params)
+
+ def test_evaluate_numeric_2d(self) -> None:
+ e = ExpressionVector([['a * b + c', 'a + d'], ['a', 3]])
+ params = {
+ 'a': 2,
+ 'b': 1.5,
+ 'c': -7,
+ 'd': 9
+ }
+ np.testing.assert_equal(np.array([[2 * 1.5 - 7, 2 + 9], [2, 3]]),
+ e.evaluate_numeric(**params))
+
+ with self.assertRaises(NonNumericEvaluation):
+ params['a'] = sympify('h')
+ e.evaluate_numeric(**params)
+
+ def test_partial_evaluation(self):
+ e = ExpressionVector(['a * b + c', 'a + d'])
+
+ params = {
+ 'a': 2,
+ 'b': 1.5,
+ 'c': -7
+ }
+
+ expected = ExpressionVector([2 * 1.5 - 7, '2 + d'])
+ evaluated = e.evaluate_symbolic(params)
+
+ np.testing.assert_equal(evaluated.underlying_expression, expected.underlying_expression)
+
+ def test_symbolic_evaluation(self):
+ e = ExpressionVector([['a * b + c', 'a + d'], ['a', 3]])
+ params = {
+ 'a': 2,
+ 'b': 1.5,
+ 'c': -7,
+ 'd': 9
+ }
+
+ expected = ExpressionVector([[2 * 1.5 - 7, 2 + 9], [2, 3]])
+ evaluated = e.evaluate_symbolic(params)
+
+ np.testing.assert_equal(evaluated.underlying_expression, expected.underlying_expression)
+
+ def test_numeric_expression(self):
+ numbers = np.linspace(1, 2, num=5)
+
+ e = ExpressionVector(numbers)
+
+ np.testing.assert_equal(e.underlying_expression, numbers)
+
+ def test_eq(self):
+ e1 = ExpressionVector([1, 2])
+ e2 = ExpressionVector(['1', '2'])
+ e3 = ExpressionVector(['1', 'a'])
+ e4 = ExpressionVector([1, 'a'])
+ e5 = ExpressionVector([1, 'a', 3])
+ e6 = ExpressionVector([1, 1, '1'])
+ e7 = ExpressionVector(['a'])
+
+ self.assertEqual(e1, e2)
+ self.assertEqual(e3, e4)
+ self.assertNotEqual(e4, e5)
+
+ self.assertEqual(e1, [1, 2])
+ self.assertNotEqual(e6, 1)
+ self.assertEqual(e7, ExpressionScalar('a'))
+
+ def test_hash(self):
+ e1 = ExpressionVector([1, 2])
+ e2 = ExpressionVector(['1', '2'])
+ e7 = ExpressionVector(['a'])
+
+ s = ExpressionScalar('a')
+ self.assertEqual({e1, e7}, {e1, e2, e7, s})
+
+ def test_pickle(self):
+ expr = ExpressionVector([1, 'a + 5', 3])
+ # populate lambdified
+ expr.evaluate_in_scope({'a': 3})
+ dumped = pickle.dumps(expr)
+ loaded = pickle.loads(dumped)
+ self.assertEqual(expr, loaded)
+
+
+class ExpressionScalarTests(unittest.TestCase):
+ def test_format(self):
+ expr = ExpressionScalar('17')
+ e_format = '{:.4e}'.format(expr)
+ self.assertEqual(e_format, "1.7000e+01")
+
+ empty_format = "{}".format(expr)
+ self.assertEqual(empty_format, '17')
+
+ expr_with_var = ExpressionScalar('17*a')
+ with self.assertRaises(TypeError):
+ # throw error on implicit float cast
+ '{:.4e}'.format(expr_with_var)
+
+ empty_format = "{}".format(expr_with_var)
+ self.assertEqual(empty_format, '17*a')
+
+ @unittest.skipIf(sys.version_info < (3, 6), "format string literals require 3.6 or higher")
+ def test_fstring(self) -> None:
+ src_code = """e = ExpressionScalar('2.0'); \
+ self.assertEqual( f'{e}', str(e) ); \
+ self.assertEqual( f'{e:.2f}', '%.2f' % e)
+ """
+ exec(src_code)
+
+ def test_evaluate_numeric(self) -> None:
+ e = ExpressionScalar('a * b + c')
+ params = {
+ 'a': 2,
+ 'b': 1.5,
+ 'c': -7
+ }
+ self.assertEqual(2 * 1.5 - 7, e.evaluate_numeric(**params))
+
+ with self.assertRaises(NonNumericEvaluation):
+ params['a'] = sympify('h')
+ e.evaluate_numeric(**params)
+
+ def test_evaluate_numpy(self):
+ e = ExpressionScalar('a * b + c')
+ params = {
+ 'a': 2*np.ones(4),
+ 'b': 1.5*np.ones(4),
+ 'c': -7*np.ones(4)
+ }
+ np.testing.assert_equal((2 * 1.5 - 7) * np.ones(4), e.evaluate_numeric(**params))
+
+ e = ExpressionScalar('a * b + c')
+ params = {
+ 'a': np.array(2),
+ 'b': np.array(1.5),
+ 'c': np.array(-7)
+ }
+ np.testing.assert_equal((2 * 1.5 - 7), e.evaluate_numeric(**params))
+
+ def test_indexing(self):
+ e = ExpressionScalar('a[i] * c')
+
+ params = {
+ 'a': np.array([1, 2, 3]),
+ 'i': 1,
+ 'c': 2
+ }
+
+ self.assertEqual(e.evaluate_numeric(**params), 2 * 2)
+ params['a'] = [1, 2, 3]
+ self.assertEqual(e.evaluate_numeric(**params), 2 * 2)
+ params['a'] = np.array([[1, 2, 3], [4, 5, 6]])
+ np.testing.assert_equal(e.evaluate_numeric(**params), 2 * np.array([4, 5, 6]))
+
+ def test_partial_evaluation(self) -> None:
+ e = ExpressionScalar('a * c')
+ params = {'c': 5.5}
+ evaluated = e.evaluate_symbolic(params)
+ expected = ExpressionScalar('a * 5.5')
+ self.assertEqual(expected.underlying_expression, evaluated.underlying_expression)
+
+ def test_partial_evaluation_vectorized(self) -> None:
+ e = ExpressionScalar('a[i] * c')
+
+ params = {
+ 'c': np.array([[1, 2], [3, 4]])
+ }
+
+ evaluated = e.evaluate_symbolic(params)
+ expected = ExpressionVector([['a[i] * 1', 'a[i] * 2'], ['a[i] * 3', 'a[i] * 4']])
+
+ np.testing.assert_equal(evaluated.underlying_expression, expected.underlying_expression)
+
+ def test_evaluate_numeric_without_numpy(self):
+ e = Expression('a * b + c')
+
+ params = {
+ 'a': 2,
+ 'b': 1.5,
+ 'c': -7
+ }
+ self.assertEqual(2 * 1.5 - 7, e.evaluate_numeric(**params))
+
+ params = {
+ 'a': 2j,
+ 'b': 1.5,
+ 'c': -7
+ }
+ self.assertEqual(2j * 1.5 - 7, e.evaluate_numeric(**params))
+
+ params = {
+ 'a': 2,
+ 'b': 6,
+ 'c': -7
+ }
+ self.assertEqual(2 * 6 - 7, e.evaluate_numeric(**params))
+
+ params = {
+ 'a': 2,
+ 'b': sympify('k'),
+ 'c': -7
+ }
+ with self.assertRaises(NonNumericEvaluation):
+ e.evaluate_numeric(**params)
+
+ def test_evaluate_symbolic(self):
+ e = ExpressionScalar('a * b + c')
+ params = {
+ 'a': 'd',
+ 'c': -7
+ }
+ result = e.evaluate_symbolic(params)
+ expected = ExpressionScalar('d*b-7')
+ self.assertEqual(result, expected)
+
+ def test_variables(self) -> None:
+ e = ExpressionScalar('4 ** pi + x * foo')
+ expected = sorted(['foo', 'x'])
+ received = sorted(e.variables)
+ self.assertEqual(expected, received)
+
+ def test_variables_indexed(self):
+ e = ExpressionScalar('a[i] * c')
+ expected = sorted(['a', 'i', 'c'])
+ received = sorted(e.variables)
+ self.assertEqual(expected, received)
+
+ def test_evaluate_variable_missing(self) -> None:
+ e = ExpressionScalar('a * b + c')
+ params = {
+ 'b': 1.5
+ }
+ with self.assertRaises(ExpressionVariableMissingException):
+ e.evaluate_numeric(**params)
+
+ def test_repr(self):
+ s = 'a * b'
+ e = ExpressionScalar(s)
+ self.assertEqual("ExpressionScalar('a * b')", repr(e))
+
+ def test_repr_original_expression_is_sympy(self):
+ # in this case we test that we get the original expression back if we do
+ # eval(repr(e))
+
+ org = sympy.sympify(3.1415)
+ e = ExpressionScalar(org)
+ self.assertEqual(e, eval(repr(e)))
+
+ org = sympy.abc.a * sympy.abc.b
+ e = ExpressionScalar(org)
+ self.assertEqual(e, eval(repr(e)))
+
+ org = sympy.sympify('3/17')
+ e = ExpressionScalar(org)
+ self.assertEqual(e, eval(repr(e)))
+
+ def test_str(self):
+ s = 'a * b'
+ e = ExpressionScalar(s)
+ self.assertEqual('a*b', str(e))
+
+ def test_original_expression(self):
+ s = 'a * b'
+ self.assertEqual(ExpressionScalar(s).original_expression, s)
+
+ def test_hash(self):
+ expected = {ExpressionScalar(2), ExpressionScalar('a')}
+ sequence = [ExpressionScalar(2), ExpressionScalar('a'), ExpressionScalar(2), ExpressionScalar('a')]
+ self.assertEqual(expected, set(sequence))
+
+ def test_undefined_comparison(self):
+ valued = ExpressionScalar(2)
+ unknown = ExpressionScalar('a')
+
+ self.assertIsNone(unknown < 0)
+ self.assertIsNone(unknown > 0)
+ self.assertIsNone(unknown >= 0)
+ self.assertIsNone(unknown <= 0)
+ self.assertFalse(unknown == 0)
+
+ self.assertIsNone(0 < unknown)
+ self.assertIsNone(0 > unknown)
+ self.assertIsNone(0 <= unknown)
+ self.assertIsNone(0 >= unknown)
+ self.assertFalse(0 == unknown)
+
+ self.assertIsNone(unknown < valued)
+ self.assertIsNone(unknown > valued)
+ self.assertIsNone(unknown >= valued)
+ self.assertIsNone(unknown <= valued)
+ self.assertFalse(unknown == valued)
+
+ valued, unknown = unknown, valued
+ self.assertIsNone(unknown < valued)
+ self.assertIsNone(unknown > valued)
+ self.assertIsNone(unknown >= valued)
+ self.assertIsNone(unknown <= valued)
+ self.assertFalse(unknown == valued)
+ valued, unknown = unknown, valued
+
+ self.assertFalse(unknown == valued)
+
+ def test_defined_comparison(self):
+ small = ExpressionScalar(2)
+ large = ExpressionScalar(3)
+
+ self.assertIs(small < small, False)
+ self.assertIs(small > small, False)
+ self.assertIs(small <= small, True)
+ self.assertIs(small >= small, True)
+ self.assertIs(small == small, True)
+
+ self.assertIs(small < large, True)
+ self.assertIs(small > large, False)
+ self.assertIs(small <= large, True)
+ self.assertIs(small >= large, False)
+ self.assertIs(small == large, False)
+
+ self.assertIs(large < small, False)
+ self.assertIs(large > small, True)
+ self.assertIs(large <= small, False)
+ self.assertIs(large >= small, True)
+ self.assertIs(large == small, False)
+
+ def test_number_comparison(self):
+ valued = ExpressionScalar(2)
+
+ self.assertIs(valued < 3, True)
+ self.assertIs(valued > 3, False)
+ self.assertIs(valued <= 3, True)
+ self.assertIs(valued >= 3, False)
+
+ self.assertIs(valued == 3, False)
+ self.assertIs(valued == 2, True)
+ self.assertIs(3 == valued, False)
+ self.assertIs(2 == valued, True)
+
+ self.assertIs(3 < valued, False)
+ self.assertIs(3 > valued, True)
+ self.assertIs(3 <= valued, False)
+ self.assertIs(3 >= valued, True)
+
+ def assertExpressionEqual(self, lhs: Expression, rhs: Expression):
+ self.assertTrue(bool(Eq(lhs, rhs)), '{} and {} are not equal'.format(lhs, rhs))
+
+ def test_number_math(self):
+ a = ExpressionScalar('a')
+ b = 3.3
+
+ self.assertExpressionEqual(a + b, b + a)
+ self.assertExpressionEqual(a - b, -(b - a))
+ self.assertExpressionEqual(a * b, b * a)
+ self.assertExpressionEqual(a / b, 1 / (b / a))
+ self.assertExpressionEqual(a // 3, ExpressionScalar('floor(a / 3)'))
+ self.assertExpressionEqual(a // 3, ExpressionScalar('a // 3'))
+ self.assertExpressionEqual(3 // a, ExpressionScalar('floor(3 / a)'))
+
+ def test_symbolic_math(self):
+ a = ExpressionScalar('a')
+ b = ExpressionScalar('b')
+
+ self.assertExpressionEqual(a + b, b + a)
+ self.assertExpressionEqual(a - b, -(b - a))
+ self.assertExpressionEqual(a * b, b * a)
+ self.assertExpressionEqual(a / b, 1 / (b / a))
+ self.assertExpressionEqual(a // b, ExpressionScalar('floor(a / b)'))
+
+ def test_sympy_math(self):
+ a = ExpressionScalar('a')
+ b = sympify('b')
+
+ self.assertExpressionEqual(a + b, b + a)
+ self.assertExpressionEqual(a - b, -(b - a))
+ self.assertExpressionEqual(a * b, b * a)
+ self.assertExpressionEqual(a / b, 1 / (b / a))
+
+ def test_is_nan(self):
+ self.assertTrue(ExpressionScalar('nan').is_nan())
+ self.assertTrue((ExpressionScalar(float('inf')) / ExpressionScalar(float('inf'))).is_nan())
+
+ self.assertFalse(ExpressionScalar(456).is_nan())
+
+ def test_special_function_numeric_evaluation(self):
+ expr = Expression('erfc(t)')
+ data = [-1., 0., 1.]
+ expected = np.array([1.84270079, 1., 0.15729921])
+ result = expr.evaluate_numeric(t=data)
+
+ np.testing.assert_allclose(expected, result)
+
+ def test_try_to_numeric(self):
+ expr = ExpressionScalar('Sum(9, (x, 0, 5), (y, 0, 7))')
+ self.assertEqual(expr._try_to_numeric(), 9*6*8)
+
+ def test_evaluate_with_exact_rationals(self):
+ expr = ExpressionScalar('1 / 3')
+ self.assertEqual(TimeType.from_fraction(1, 3), expr.evaluate_with_exact_rationals({}))
+
+ expr = ExpressionScalar('a * (1 / 3)')
+ self.assertEqual(TimeType.from_fraction(2, 3), expr.evaluate_with_exact_rationals({'a': 2}))
+
+ expr = ExpressionScalar('dot(a, b) * (1 / 3)')
+ self.assertEqual(TimeType.from_fraction(10, 3),
+ expr.evaluate_with_exact_rationals({'a': [2, 2], 'b': [1, 4]}))
+
+ def test_pickle(self):
+ expr = ExpressionScalar('1 / a')
+ # populate lambdified
+ expr.evaluate_in_scope({'a': 7})
+ dumped = pickle.dumps(expr)
+ loaded = pickle.loads(dumped)
+ self.assertEqual(expr, loaded)
+
+ def test_roundtrip(self):
+ original = ExpressionScalar("a * 16") / ExpressionScalar(12 / 5) * ExpressionScalar(4)
+ serialized = original.get_serialization_data()
+ deserialized = ExpressionScalar(serialized)
+ self.assertEqual(original, deserialized)
+
+
+class ExpressionExceptionTests(unittest.TestCase):
+ def test_expression_variable_missing(self):
+ variable = 's'
+ expression = ExpressionScalar('s*t')
+
+ self.assertEqual(str(ExpressionVariableMissingException(variable, expression)),
+ "Could not evaluate : A value for variable is missing!")
+
+ def test_non_numeric_evaluation(self):
+ expression = ExpressionScalar('a*b')
+ call_arguments = dict()
+
+ expected = "The result of evaluate_numeric is of type {} " \
+ "which is not a number".format(float)
+ self.assertEqual(str(NonNumericEvaluation(expression, 1., call_arguments)), expected)
+
+ expected = "The result of evaluate_numeric is of type {} " \
+ "which is not a number".format(np.zeros(1).dtype)
+ self.assertEqual(str(NonNumericEvaluation(expression, np.zeros(1), call_arguments)), expected)
diff --git a/tests/hardware/alazar_tests.py b/tests/hardware/alazar_tests.py
index c6e7d032d..034f05254 100644
--- a/tests/hardware/alazar_tests.py
+++ b/tests/hardware/alazar_tests.py
@@ -6,7 +6,7 @@
from ..hardware import *
from qupulse.hardware.dacs.alazar import AlazarCard, AlazarProgram
from qupulse.utils.types import TimeType
-
+from qupulse.utils.performance import WindowOverlapWarning
class AlazarProgramTest(unittest.TestCase):
def setUp(self) -> None:
@@ -112,7 +112,7 @@ def test_make_mask(self):
with self.assertRaises(KeyError):
card._make_mask('N', begins, lengths)
- with self.assertRaises(ValueError):
+ with self.assertWarns(WindowOverlapWarning):
card._make_mask('M', begins, lengths*3)
mask = card._make_mask('M', begins, lengths)
diff --git a/tests/hardware/base_tests.py b/tests/hardware/base_tests.py
index a3f029618..0d61c20be 100644
--- a/tests/hardware/base_tests.py
+++ b/tests/hardware/base_tests.py
@@ -5,7 +5,7 @@
import numpy as np
from qupulse.utils.types import TimeType
-from qupulse._program._loop import Loop
+from qupulse.program.loop import Loop
from qupulse.hardware.awgs.base import ProgramEntry
from tests.pulses.sequencing_dummies import DummyWaveform
@@ -43,39 +43,42 @@ def test_init(self):
expected_waveforms = OrderedDict(zip(self.waveforms, sampled))
with mock.patch.object(ProgramEntry, '_sample_waveforms', return_value=sampled) as sample_waveforms:
- entry = ProgramEntry(loop=self.loop,
+ entry = ProgramEntry(program=self.loop,
channels=self.channels,
markers=self.marker,
amplitudes=self.amplitudes,
offsets=self.offset,
voltage_transformations=self.voltage_transformations,
sample_rate=self.sample_rate,
- waveforms=[])
+ waveforms=[],
+ )
self.assertIs(self.loop, entry._loop)
self.assertEqual(0, len(entry._waveforms))
sample_waveforms.assert_not_called()
with mock.patch.object(ProgramEntry, '_sample_waveforms', return_value=sampled) as sample_waveforms:
- entry = ProgramEntry(loop=self.loop,
+ entry = ProgramEntry(program=self.loop,
channels=self.channels,
markers=self.marker,
amplitudes=self.amplitudes,
offsets=self.offset,
voltage_transformations=self.voltage_transformations,
sample_rate=self.sample_rate,
- waveforms=None)
+ waveforms=None,
+ )
self.assertEqual(expected_waveforms, entry._waveforms)
sample_waveforms.assert_called_once_with(expected_default)
with mock.patch.object(ProgramEntry, '_sample_waveforms', return_value=sampled[:1]) as sample_waveforms:
- entry = ProgramEntry(loop=self.loop,
+ entry = ProgramEntry(program=self.loop,
channels=self.channels,
markers=self.marker,
amplitudes=self.amplitudes,
offsets=self.offset,
voltage_transformations=self.voltage_transformations,
sample_rate=self.sample_rate,
- waveforms=self.waveforms[:1])
+ waveforms=self.waveforms[:1],
+ )
self.assertEqual(OrderedDict([(self.waveforms[0], sampled[0])]), entry._waveforms)
sample_waveforms.assert_called_once_with(self.waveforms[:1])
@@ -89,14 +92,15 @@ def test_sample_waveforms(self):
((self.sampled[1]['A'], empty_ch, 2.*(self.sampled[1]['C'] - 0.1)), (empty_m, self.sampled[1]['M'] != 0))
]
- entry = ProgramEntry(loop=self.loop,
+ entry = ProgramEntry(program=self.loop,
channels=self.channels,
markers=self.marker,
amplitudes=self.amplitudes,
offsets=self.offset,
voltage_transformations=self.voltage_transformations,
sample_rate=self.sample_rate,
- waveforms=[])
+ waveforms=[],
+ )
with mock.patch.object(entry, '_sample_empty_channel', return_value=empty_ch):
with mock.patch.object(entry, '_sample_empty_marker', return_value=empty_m):
diff --git a/tests/hardware/dummy_devices.py b/tests/hardware/dummy_devices.py
index a92ce1282..60dee6f9e 100644
--- a/tests/hardware/dummy_devices.py
+++ b/tests/hardware/dummy_devices.py
@@ -1,126 +1,2 @@
-from typing import Tuple, Set, Dict
-from collections import deque
-
-
-from qupulse.hardware.awgs.base import AWG, ProgramOverwriteException
-from qupulse.hardware.dacs import DAC
-
-class DummyDAC(DAC):
- def __init__(self):
- self._measurement_windows = dict()
- self._operations = dict()
- self.measured_data = deque([])
- self._meas_masks = {}
- self._armed_program = None
-
- @property
- def armed_program(self):
- return self._armed_program
-
- def register_measurement_windows(self, program_name: str, windows: Dict[str, Tuple['numpy.ndarray',
- 'numpy.ndarray']]):
- self._measurement_windows[program_name] = windows
-
- def register_operations(self, program_name: str, operations):
- self._operations[program_name] = operations
-
- def arm_program(self, program_name: str):
- self._armed_program = program_name
-
- def delete_program(self, program_name):
- if program_name in self._operations:
- self._operations.pop(program_name)
- if program_name in self._measurement_windows:
- self._measurement_windows.pop(program_name)
-
- def clear(self) -> None:
- self._measurement_windows = dict()
- self._operations = dict()
- self._armed_program = None
-
- def measure_program(self, channels):
- return self.measured_data.pop()
-
- def set_measurement_mask(self, program_name, mask_name, begins, lengths) -> Tuple['numpy.ndarray', 'numpy.ndarray']:
- self._meas_masks.setdefault(program_name, {})[mask_name] = (begins, lengths)
- return begins, lengths
-
-
-class DummyAWG(AWG):
- """Dummy AWG for debugging purposes."""
-
- def __init__(self,
- memory: int=100,
- sample_rate: float=10,
- output_range: Tuple[float, float]=(-5, 5),
- num_channels: int=1,
- num_markers: int=1) -> None:
- """Create a new DummyAWG instance.
-
- Args:
- memory (int): Available memory slots for waveforms. (default = 100)
- sample_rate (float): The sample rate of the dummy. (default = 10)
- output_range (float, float): A (min,max)-tuple of possible output values.
- (default = (-5,5)).
- """
- super().__init__(identifier="DummyAWG{0}".format(id(self)))
-
- self._programs = {} # contains program names and programs
- self._sample_rate = sample_rate
- self._output_range = output_range
- self._num_channels = num_channels
- self._num_markers = num_markers
- self._channels = ('default',)
- self._armed = None
-
- # todo [2018-06-14]: The following attributes (and thus the memory argument) are never used. Remove?
- self._waveform_memory = [None for i in range(memory)]
- self._waveform_indices = {} # dict that maps from waveform hash to memory index
- self._program_wfs = {} # contains program names and necessary waveforms indices
-
- def set_volatile_parameters(self, program_name: str, parameters):
- raise NotImplementedError()
-
- def upload(self, name, program, channels, markers, voltage_transformation, force=False) -> None:
- if name in self.programs:
- if not force:
- raise ProgramOverwriteException(name)
- else:
- self.remove(name)
- self.upload(name, program)
- else:
- self._programs[name] = (program, channels, markers, voltage_transformation)
-
- def remove(self, name) -> None:
- if name in self.programs:
- self._programs.pop(name)
-
- def clear(self) -> None:
- self._programs = {}
-
- def arm(self, name: str) -> None:
- self._armed = name
-
- @property
- def programs(self) -> Set[str]:
- return set(self._programs.keys())
-
- @property
- def output_range(self) -> Tuple[float, float]:
- return self._output_range
-
- @property
- def identifier(self) -> str:
- return "DummyAWG{0}".format(id(self))
-
- @property
- def sample_rate(self) -> float:
- return self._sample_rate
-
- @property
- def num_channels(self):
- return self._num_channels
-
- @property
- def num_markers(self):
- return self._num_markers
\ No newline at end of file
+from qupulse.hardware.dacs.dummy import DummyDAC
+from qupulse.hardware.awgs.dummy import DummyAWG
diff --git a/tests/hardware/feature_awg/awg_new_driver_base_tests.py b/tests/hardware/feature_awg/awg_new_driver_base_tests.py
index 12070e1c1..1845ab1e0 100644
--- a/tests/hardware/feature_awg/awg_new_driver_base_tests.py
+++ b/tests/hardware/feature_awg/awg_new_driver_base_tests.py
@@ -3,7 +3,7 @@
import warnings
from qupulse import ChannelID
-from qupulse._program._loop import Loop
+from qupulse.program.loop import Loop
from qupulse.hardware.feature_awg import channel_tuple_wrapper
from qupulse.hardware.feature_awg.base import AWGDevice, AWGChannel, AWGChannelTuple, AWGMarkerChannel
from qupulse.hardware.feature_awg.features import ChannelSynchronization, ProgramManagement, VoltageRange, \
diff --git a/tests/hardware/setup_tests.py b/tests/hardware/setup_tests.py
index d36467e23..fd3ba3c17 100644
--- a/tests/hardware/setup_tests.py
+++ b/tests/hardware/setup_tests.py
@@ -4,7 +4,7 @@
import numpy as np
from qupulse.hardware.setup import HardwareSetup, PlaybackChannel, MarkerChannel, MeasurementMask
-from qupulse._program._loop import Loop
+from qupulse.program.loop import Loop
from tests.pulses.sequencing_dummies import DummyWaveform
diff --git a/tests/hardware/tabor_clock_tests.py b/tests/hardware/tabor_clock_tests.py
index 25270284b..c2baa52bf 100644
--- a/tests/hardware/tabor_clock_tests.py
+++ b/tests/hardware/tabor_clock_tests.py
@@ -17,7 +17,6 @@ def get_pulse():
marker_on = FPT('1', 'tau', channel='trigger')
multi = MPT([sine, marker_on], {'tau', 'U'})
- multi.atomicity = True
assert sine.defined_channels == {'out'}
assert multi.defined_channels == {'out', 'trigger'}
diff --git a/tests/hardware/tabor_dummy_based_tests.py b/tests/hardware/tabor_dummy_based_tests.py
index 4bc3f84fa..98588b5ed 100644
--- a/tests/hardware/tabor_dummy_based_tests.py
+++ b/tests/hardware/tabor_dummy_based_tests.py
@@ -10,7 +10,7 @@
from qupulse.hardware.awgs.base import AWGAmplitudeOffsetHandling
from qupulse.utils.types import TimeType
-from qupulse._program._loop import Loop
+from qupulse.program.loop import Loop
from qupulse._program.tabor import TableDescription, TimeType, TableEntry, TaborSegment, TaborProgram,\
make_combined_wave, TaborSequencing
from qupulse._program.waveforms import ConstantWaveform
@@ -461,97 +461,6 @@ def test_upload_offset_handling(self):
voltage_transformations=test_transform)
self.assertEqual([], offset_mock.call_args_list)
- def test_find_place_for_segments_in_memory(self):
- def hash_based_on_dir(ch):
- hash_list = []
- for d in dir(ch):
- o = getattr(ch, d)
- if isinstance(o, np.ndarray):
- hash_list.append(hash(o.tobytes()))
- else:
- try:
- hash_list.append(hash(o))
- except TypeError:
- pass
- return hash(tuple(hash_list))
-
- channel_pair = TaborChannelPair(self.instrument, identifier='asd', channels=(1, 2))
-
- # empty
- segments = np.asarray([-5, -6, -7, -8, -9])
- segment_lengths = 192 + np.asarray([32, 16, 64, 32, 16])
-
- hash_before = hash_based_on_dir(channel_pair)
-
- w2s, ta, ti = channel_pair._find_place_for_segments_in_memory(segments, segment_lengths)
- self.assertEqual(w2s.tolist(), [-1, -1, -1, -1, -1])
- self.assertEqual(ta.tolist(), [True, True, True, True, True])
- self.assertEqual(ti.tolist(), [-1, -1, -1, -1, -1])
- self.assertEqual(hash_before, hash_based_on_dir(channel_pair))
-
- # all new segments
- channel_pair._segment_capacity = 192 + np.asarray([0, 16, 32, 16, 0], dtype=np.uint32)
- channel_pair._segment_hashes = np.asarray([1, 2, 3, 4, 5], dtype=np.int64)
- channel_pair._segment_references = np.asarray([1, 1, 1, 2, 1], dtype=np.int32)
- hash_before = hash_based_on_dir(channel_pair)
-
- w2s, ta, ti = channel_pair._find_place_for_segments_in_memory(segments, segment_lengths)
- self.assertEqual(w2s.tolist(), [-1, -1, -1, -1, -1])
- self.assertEqual(ta.tolist(), [True, True, True, True, True])
- self.assertEqual(ti.tolist(), [-1, -1, -1, -1, -1])
- self.assertEqual(hash_before, hash_based_on_dir(channel_pair))
-
- # some known segments
- channel_pair._segment_capacity = 192 + np.asarray([0, 16, 32, 64, 0, 16], dtype=np.uint32)
- channel_pair._segment_hashes = np.asarray([1, 2, 3, -7, 5, -9], dtype=np.int64)
- channel_pair._segment_references = np.asarray([1, 1, 1, 2, 1, 3], dtype=np.int32)
- hash_before = hash_based_on_dir(channel_pair)
-
- w2s, ta, ti = channel_pair._find_place_for_segments_in_memory(segments, segment_lengths)
- self.assertEqual(w2s.tolist(), [-1, -1, 3, -1, 5])
- self.assertEqual(ta.tolist(), [True, True, False, True, False])
- self.assertEqual(ti.tolist(), [-1, -1, -1, -1, -1])
- self.assertEqual(hash_before, hash_based_on_dir(channel_pair))
-
- # insert some segments with same length
- channel_pair._segment_capacity = 192 + np.asarray([0, 16, 32, 64, 0, 16], dtype=np.uint32)
- channel_pair._segment_hashes = np.asarray([1, 2, 3, 4, 5, 6], dtype=np.int64)
- channel_pair._segment_references = np.asarray([1, 0, 1, 0, 1, 3], dtype=np.int32)
- hash_before = hash_based_on_dir(channel_pair)
-
- w2s, ta, ti = channel_pair._find_place_for_segments_in_memory(segments, segment_lengths)
- self.assertEqual(w2s.tolist(), [-1, -1, -1, -1, -1])
- self.assertEqual(ta.tolist(), [True, False, False, True, True])
- self.assertEqual(ti.tolist(), [-1, 1, 3, -1, -1])
- self.assertEqual(hash_before, hash_based_on_dir(channel_pair))
-
- # insert some segments with smaller length
- channel_pair._segment_capacity = 192 + np.asarray([0, 80, 32, 64, 96, 16], dtype=np.uint32)
- channel_pair._segment_hashes = np.asarray([1, 2, 3, 4, 5, 6], dtype=np.int64)
- channel_pair._segment_references = np.asarray([1, 0, 1, 1, 0, 3], dtype=np.int32)
- hash_before = hash_based_on_dir(channel_pair)
-
- w2s, ta, ti = channel_pair._find_place_for_segments_in_memory(segments, segment_lengths)
- self.assertEqual(w2s.tolist(), [-1, -1, -1, -1, -1])
- self.assertEqual(ta.tolist(), [True, True, False, False, True])
- self.assertEqual(ti.tolist(), [-1, -1, 4, 1, -1])
- self.assertEqual(hash_before, hash_based_on_dir(channel_pair))
-
- # mix everything
- segments = np.asarray([-5, -6, -7, -8, -9, -10, -11])
- segment_lengths = 192 + np.asarray([32, 16, 64, 32, 16, 0, 0])
-
- channel_pair._segment_capacity = 192 + np.asarray([0, 80, 32, 64, 32, 16], dtype=np.uint32)
- channel_pair._segment_hashes = np.asarray([1, 2, 3, 4, -8, 6], dtype=np.int64)
- channel_pair._segment_references = np.asarray([1, 0, 1, 0, 1, 0], dtype=np.int32)
- hash_before = hash_based_on_dir(channel_pair)
-
- w2s, ta, ti = channel_pair._find_place_for_segments_in_memory(segments, segment_lengths)
- self.assertEqual(w2s.tolist(), [-1, -1, -1, 4, -1, -1, -1])
- self.assertEqual(ta.tolist(), [False, True, False, False, True, True, True])
- self.assertEqual(ti.tolist(), [1, -1, 3, -1, -1, -1, -1])
- self.assertEqual(hash_before, hash_based_on_dir(channel_pair))
-
def test_upload_segment(self):
channel_pair = TaborChannelPair(self.instrument, identifier='asd', channels=(1, 2))
diff --git a/tests/hardware/tabor_simulator_based_tests.py b/tests/hardware/tabor_simulator_based_tests.py
index 97a715424..ace3b2fbc 100644
--- a/tests/hardware/tabor_simulator_based_tests.py
+++ b/tests/hardware/tabor_simulator_based_tests.py
@@ -7,12 +7,16 @@
try:
import pyvisa.resources
import tabor_control
-except ImportError as err:
- raise unittest.SkipTest("pyvisa and/or tabor_control not present") from err
+except ImportError:
+ tabor_control = None
+ pyvisa = None
import numpy as np
-from qupulse.hardware.awgs.tabor import TaborAWGRepresentation, TaborChannelPair
+try:
+ from qupulse.hardware.awgs.tabor import TaborAWGRepresentation, TaborChannelPair
+except ImportError:
+ pass
from qupulse._program.tabor import TaborSegment, PlottableProgram, TaborException, TableDescription, TableEntry
from typing import List, Tuple, Optional, Any
@@ -48,7 +52,7 @@ def kill_running_simulators(self):
def simulator_full_path(self):
return os.path.join(self.simulator_path, self.simulator_executable)
- def start_simulator(self, try_connecting_to_existing_simulator=True, max_wait_time=30) -> pyvisa.resources.MessageBasedResource:
+ def start_simulator(self, try_connecting_to_existing_simulator=True, max_wait_time=30) -> 'pyvisa.resources.MessageBasedResource':
try:
pyvisa.ResourceManager()
except ValueError:
@@ -95,7 +99,7 @@ def __del__(self):
self.simulator_process.kill()
-@unittest.skipIf(platform.system() != 'Windows', "Simulator currently only available on Windows :(")
+@unittest.skipIf(tabor_control is None or platform.system() != 'Windows', "Simulator currently only available on Windows :(")
class TaborSimulatorBasedTest(unittest.TestCase):
simulator_manager = None
diff --git a/tests/hardware/tabor_tests.py b/tests/hardware/tabor_tests.py
index 6226b2c63..e9bf6e5bf 100644
--- a/tests/hardware/tabor_tests.py
+++ b/tests/hardware/tabor_tests.py
@@ -9,7 +9,7 @@
from qupulse.hardware.awgs.tabor import TaborException, TaborProgram, \
TaborSegment, TaborSequencing, with_configuration_guard, PlottableProgram
-from qupulse._program._loop import Loop
+from qupulse.program.loop import Loop
from qupulse.hardware.util import voltage_to_uint16
from tests.pulses.sequencing_dummies import DummyWaveform
diff --git a/tests/hardware/tektronix_tests.py b/tests/hardware/tektronix_tests.py
index a1f326b65..aed67d79e 100644
--- a/tests/hardware/tektronix_tests.py
+++ b/tests/hardware/tektronix_tests.py
@@ -13,7 +13,7 @@
from qupulse.hardware.awgs.tektronix import TektronixAWG, TektronixProgram, parse_program, _make_binary_waveform,\
voltage_to_uint16, WaveformEntry, WaveformStorage
-from qupulse._program._loop import Loop
+from qupulse.program.loop import Loop
from qupulse.utils.types import TimeType
from tests.pulses.sequencing_dummies import DummyWaveform
from qupulse._program.waveforms import MultiChannelWaveform
@@ -324,10 +324,6 @@ def test_init(self):
init_idle_patch = mock.patch('qupulse.hardware.awgs.tektronix.TektronixAWG.initialize_idle_program')
synchronize_patch = mock.patch('qupulse.hardware.awgs.tektronix.TektronixAWG.synchronize')
- with mock.patch('qupulse.hardware.awgs.tektronix.tek_awg', new=None):
- with self.assertRaisesRegex(RuntimeError, 'tek_awg'):
- TektronixAWG(self.make_dummy_tek_awg(), 'clear')
-
with self.patch_method('make_idle_waveform') as make_idle_waveform:
with self.assertRaisesRegex(ValueError, 'synchronize'):
TektronixAWG(self.make_dummy_tek_awg(), 'foo', idle_waveform_length=300)
diff --git a/tests/hardware/util_tests.py b/tests/hardware/util_tests.py
index 9b983c28f..7693ca3cb 100644
--- a/tests/hardware/util_tests.py
+++ b/tests/hardware/util_tests.py
@@ -98,6 +98,8 @@ def test_size_exception(self):
def test_range_exception(self):
with self.assertRaisesRegex(ValueError, "invalid"):
zhinst_voltage_to_uint16(2.*np.ones(192), None, (None, None, None, None))
+ with self.assertRaisesRegex(ValueError, "invalid"):
+ zhinst_voltage_to_uint16(None, 2.*np.ones(192), (None, None, None, None))
# this should work
zhinst_voltage_to_uint16(None, None, (2. * np.ones(192), None, None, None))
diff --git a/tests/hardware/zihdawg_tests.py b/tests/hardware/zihdawg_tests.py
deleted file mode 100644
index 9fe675eab..000000000
--- a/tests/hardware/zihdawg_tests.py
+++ /dev/null
@@ -1,217 +0,0 @@
-import unittest
-from unittest import mock
-from collections import OrderedDict
-
-import numpy as np
-
-try:
- import pytest
-except ImportError:
- pytest = None
-
-if pytest:
- zhinst = pytest.importorskip("zhinst")
-
- try:
- import zhinst.core as zhinst_core
- except ImportError:
- import zhinst.ziPython as zhinst_core
-else:
- try:
- try:
- import zhinst.core as zhinst_core
- except ImportError:
- import zhinst.ziPython as zhinst_core
- except ImportError as err:
- raise unittest.SkipTest("zhinst not present") from err
-
-from qupulse.utils.types import TimeType
-from qupulse._program._loop import Loop
-from tests.pulses.sequencing_dummies import DummyWaveform
-from qupulse.hardware.awgs.zihdawg import HDAWGChannelGroup, HDAWGRepresentation, HDAWGValueError, UserRegister,\
- ELFManager, HDAWGChannelGrouping, SingleDeviceChannelGroup
-
-
-class HDAWGRepresentationTests(unittest.TestCase):
- def test_init(self):
- """We do not test anything lab one related"""
- device_serial = 'dev6ä6ä6'
- device_interface = 'telepathy'
- data_server_addr = 'asd'
- data_server_port = 42
- api_level_number = 23
- channel_grouping = HDAWGChannelGrouping.CHAN_GROUP_1x8
-
- with \
- mock.patch('zhinst.utils.api_server_version_check') as mock_version_check,\
- mock.patch.object(zhinst_core, 'ziDAQServer') as mock_daq_server, \
- mock.patch('qupulse.hardware.awgs.zihdawg.HDAWGRepresentation._initialize') as mock_init, \
- mock.patch('qupulse.hardware.awgs.zihdawg.HDAWGRepresentation.channel_grouping', new_callable=mock.PropertyMock) as mock_grouping, \
- mock.patch('qupulse.hardware.awgs.zihdawg.SingleDeviceChannelGroup') as mock_channel_pair,\
- mock.patch('zhinst.utils.disable_everything') as mock_reset,\
- mock.patch('pathlib.Path') as mock_path:
-
- representation = HDAWGRepresentation(device_serial,
- device_interface,
- data_server_addr, data_server_port, api_level_number,
- False, 1.3, grouping=channel_grouping)
-
- mock_daq_server.return_value.awgModule.return_value.getString.assert_called_once_with('directory')
- module_dir = mock_daq_server.return_value.awgModule.return_value.getString.return_value
- mock_path.assert_called_once_with(module_dir, 'awg', 'waves')
-
- self.assertIs(representation.api_session, mock_daq_server.return_value)
- mock_daq_server.assert_called_once_with(data_server_addr, data_server_port, api_level_number)
-
- mock_version_check.assert_called_once_with(representation.api_session)
- representation.api_session.connectDevice.assert_called_once_with(device_serial, device_interface)
- self.assertEqual(device_serial, representation.serial)
-
- mock_grouping.assert_called_once_with(channel_grouping)
-
- mock_reset.assert_not_called()
- mock_init.assert_called_once_with()
-
- group_calls = [mock.call(0, 2, identifier=str(device_serial) + '_AB', timeout=1.3),
- mock.call(1, 2, identifier=str(device_serial) + '_CD', timeout=1.3),
- mock.call(2, 2, identifier=str(device_serial) + '_EF', timeout=1.3),
- mock.call(3, 2, identifier=str(device_serial) + '_GH', timeout=1.3),
- mock.call(0, 4, identifier=str(device_serial) + '_ABCD', timeout=1.3),
- mock.call(1, 4, identifier=str(device_serial) + '_EFGH', timeout=1.3),
- mock.call(0, 8, identifier=str(device_serial) + '_ABCDEFGH', timeout=1.3)]
- for c1, c2 in zip(group_calls, mock_channel_pair.call_args_list):
- self.assertEqual(c1, c2)
-
- self.assertIs(representation.channel_pair_AB, mock_channel_pair.return_value)
- self.assertIs(representation.channel_pair_CD, mock_channel_pair.return_value)
- self.assertIs(representation.channel_pair_EF, mock_channel_pair.return_value)
- self.assertIs(representation.channel_pair_GH, mock_channel_pair.return_value)
-
- mock_version_check.reset_mock()
- mock_daq_server.reset_mock()
- mock_init.reset_mock()
- mock_channel_pair.reset_mock()
- mock_reset.reset_mock()
-
- representation = HDAWGRepresentation(device_serial,
- device_interface,
- data_server_addr, data_server_port, api_level_number, True)
-
- self.assertIs(representation.api_session, mock_daq_server.return_value)
- mock_daq_server.assert_called_once_with(data_server_addr, data_server_port, api_level_number)
-
- mock_version_check.assert_called_once_with(representation.api_session)
- representation.api_session.connectDevice.assert_called_once_with(device_serial, device_interface)
- self.assertEqual(device_serial, representation.serial)
-
- mock_reset.assert_called_once_with(representation.api_session, representation.serial)
- mock_init.assert_called_once_with()
-
- group_calls = [mock.call(0, 2, identifier=str(device_serial) + '_AB', timeout=20),
- mock.call(1, 2, identifier=str(device_serial) + '_CD', timeout=20),
- mock.call(2, 2, identifier=str(device_serial) + '_EF', timeout=20),
- mock.call(3, 2, identifier=str(device_serial) + '_GH', timeout=20),
- mock.call(0, 4, identifier=str(device_serial) + '_ABCD', timeout=20),
- mock.call(1, 4, identifier=str(device_serial) + '_EFGH', timeout=20),
- mock.call(0, 8, identifier=str(device_serial) + '_ABCDEFGH', timeout=20)]
- self.assertEqual(group_calls, mock_channel_pair.call_args_list)
-
- self.assertIs(representation.channel_pair_AB, mock_channel_pair.return_value)
- self.assertIs(representation.channel_pair_CD, mock_channel_pair.return_value)
- self.assertIs(representation.channel_pair_EF, mock_channel_pair.return_value)
- self.assertIs(representation.channel_pair_GH, mock_channel_pair.return_value)
-
-
-class HDAWGChannelGroupTests(unittest.TestCase):
- def test_init(self):
- with mock.patch('weakref.proxy') as proxy_mock:
- mock_device = mock.Mock()
-
- channels = (3, 4)
- awg_group_idx = 1
-
- channel_pair = SingleDeviceChannelGroup(awg_group_idx, 2, 'foo', 3.4)
-
- self.assertEqual(channel_pair.timeout, 3.4)
- self.assertEqual(channel_pair._channels(), channels)
- self.assertEqual(channel_pair.awg_group_index, awg_group_idx)
- self.assertEqual(channel_pair.num_channels, 2)
- self.assertEqual(channel_pair.num_markers, 4)
-
- self.assertFalse(channel_pair.is_connected())
-
- proxy_mock.return_value.channel_grouping = HDAWGChannelGrouping.CHAN_GROUP_4x2
-
- channel_pair.connect_group(mock_device)
- self.assertTrue(channel_pair.is_connected())
- proxy_mock.assert_called_once_with(mock_device)
- self.assertIs(channel_pair.master_device, proxy_mock.return_value)
- self.assertIs(channel_pair.awg_module, channel_pair.master_device.api_session.awgModule.return_value)
-
- def test_set_volatile_parameters(self):
- mock_device = mock.Mock()
-
- parameters = {'a': 9}
- requested_changes = OrderedDict([(UserRegister.from_seqc(4), 2), (UserRegister.from_seqc(3), 6)])
-
- expected_user_reg_calls = [mock.call(*args) for args in requested_changes.items()]
-
- channel_pair = SingleDeviceChannelGroup(1, 2, 'foo', 3.4)
-
- channel_pair._current_program = 'active_program'
- with mock.patch.object(channel_pair._program_manager, 'get_register_values_to_update_volatile_parameters',
- return_value=requested_changes) as get_reg_val:
- with mock.patch.object(channel_pair, 'user_register') as user_register:
- channel_pair.set_volatile_parameters('other_program', parameters)
-
- user_register.assert_not_called()
- get_reg_val.assert_called_once_with('other_program', parameters)
-
- with mock.patch.object(channel_pair._program_manager, 'get_register_values_to_update_volatile_parameters',
- return_value=requested_changes) as get_reg_val:
- with mock.patch.object(channel_pair, 'user_register') as user_register:
- channel_pair.set_volatile_parameters('active_program', parameters)
-
- self.assertEqual(expected_user_reg_calls, user_register.call_args_list)
- get_reg_val.assert_called_once_with('active_program', parameters)
-
- def test_upload(self):
- mock_loop = mock.MagicMock(wraps=Loop(repetition_count=2,
- waveform=DummyWaveform(duration=192,
- sample_output=np.arange(192) / 192)))
-
- voltage_trafos = (lambda x: x, lambda x: x)
-
- with mock.patch('weakref.proxy'),\
- mock.patch('qupulse.hardware.awgs.zihdawg.make_compatible') as mock_make_compatible:
- channel_pair = SingleDeviceChannelGroup(1, 2, 'foo', 3.4)
-
- with self.assertRaisesRegex(HDAWGValueError, 'Channel ID'):
- channel_pair.upload('bar', mock_loop, ('A'), (None, 'A', None, None), voltage_trafos)
- with self.assertRaisesRegex(HDAWGValueError, 'Markers'):
- channel_pair.upload('bar', mock_loop, ('A', None), (None, 'A', None), voltage_trafos)
- with self.assertRaisesRegex(HDAWGValueError, 'transformations'):
- channel_pair.upload('bar', mock_loop, ('A', None), (None, 'A', None, None), voltage_trafos[:1])
-
- # TODO: draw the rest of the owl
-
-
-@mock.patch('qupulse.hardware.awgs.zihdawg.ELFManager.AWGModule.compiler_upload', new_callable=mock.PropertyMock)
-class ELFManagerTests(unittest.TestCase):
- def test_init(self, compiler_upload):
- manager = ELFManager(None)
- compiler_upload.assert_called_once_with(True)
- self.assertIsNone(manager._compile_job)
- self.assertIsNone(manager._upload_job)
-
- @unittest.skip("Write test after more hardware tests")
- def test_upload(self, compiler_upload):
- raise NotImplementedError()
-
- @unittest.skip("Write test after more hardware tests")
- def test_update_compile_job_status(self, compiler_upload):
- raise NotImplementedError()
-
- @unittest.skip("Write test after more hardware tests")
- def test_compile(self, compiler_upload):
- raise NotImplementedError()
diff --git a/tests/program/__init__.py b/tests/program/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/program/linspace_tests.py b/tests/program/linspace_tests.py
new file mode 100644
index 000000000..deab804ab
--- /dev/null
+++ b/tests/program/linspace_tests.py
@@ -0,0 +1,643 @@
+import copy
+import unittest
+from unittest import TestCase
+
+from qupulse.pulses import *
+from qupulse.program.linspace import *
+from qupulse.program.transformation import *
+from qupulse.pulses.function_pulse_template import FunctionPulseTemplate
+
+
+def assert_vm_output_almost_equal(test: TestCase, expected, actual):
+ """Compare two vm outputs with default TestCase.assertAlmostEqual accuracy"""
+ test.assertEqual(len(expected), len(actual))
+ for idx, ((t_e, vals_e), (t_a, vals_a)) in enumerate(zip(expected, actual)):
+ test.assertEqual(t_e, t_a, f"Differing times in {idx}th element")
+ test.assertEqual(len(vals_e), len(vals_a), f"Differing channel count in {idx} element")
+ for ch, (val_e, val_a) in enumerate(zip(vals_e, vals_a)):
+ test.assertAlmostEqual(val_e, val_a, msg=f"Differing values in {idx} of {len(expected)} element channel {ch}")
+
+
+class SingleRampTest(TestCase):
+ def setUp(self):
+ hold = ConstantPT(10 ** 6, {'a': '-1. + idx * 0.01'})
+ self.pulse_template = hold.with_iteration('idx', 200)
+
+ self.program = LinSpaceProgram((LinSpaceIter(
+ length=200,
+ body=(LinSpaceHold(
+ bases=(-1.,),
+ factors=((0.01,),),
+ duration_base=TimeType(10**6),
+ duration_factors=None
+ ),)
+ ),), ("a",))
+
+ key = DepKey.from_voltages((0.01,), DEFAULT_INCREMENT_RESOLUTION)
+
+ self.commands = [
+ Set(0, -1.0, key),
+ Wait(TimeType(10 ** 6)),
+ LoopLabel(0, 199),
+ Increment(0, 0.01, key),
+ Wait(TimeType(10 ** 6)),
+ LoopJmp(0)
+ ]
+
+ self.output = [
+ (TimeType(10**6 * idx), [sum([-1.0] + [0.01] * idx)]) for idx in range(200)
+ ]
+
+ def test_program(self):
+ program_builder = LinSpaceBuilder(('a',))
+ program = self.pulse_template.create_program(program_builder=program_builder)
+ self.assertEqual(self.program, program)
+
+ def test_commands(self):
+ commands = to_increment_commands(self.program)
+ self.assertEqual(self.commands, commands)
+
+ def test_output(self):
+ vm = LinSpaceVM(1)
+ vm.set_commands(commands=self.commands)
+ vm.run()
+ assert_vm_output_almost_equal(self, self.output, vm.history)
+
+
+class SequencedRepetitionTest(TestCase):
+ def setUp(self):
+
+ base_time = 1e2
+ rep_factor = 2
+
+ wait = AtomicMultiChannelPT(
+ ConstantPT(f'{base_time}', {'a': '-1. + idx_a * 0.01', }),
+ ConstantPT(f'{base_time}', {'b': '-0.5 + idx_b * 0.05'})
+ )
+
+ dependent_constant = AtomicMultiChannelPT(
+ ConstantPT(base_time, {'a': '-1.0 '}),
+ ConstantPT(base_time, {'b': '-0.5 + idx_b*0.05',}),
+ )
+
+ dependent_constant2 = AtomicMultiChannelPT(
+ ConstantPT(base_time, {'a': '-0.5 '}),
+ ConstantPT(base_time, {'b': '-0.3 + idx_b*0.05',}),
+ )
+
+ #not working
+ self.pulse_template = (
+ dependent_constant @
+ dependent_constant2.with_repetition(rep_factor) @
+ wait.with_iteration('idx_a', rep_factor)
+ ).with_iteration('idx_b', rep_factor)
+
+ wait_hold = LinSpaceHold(
+ bases=(-1.0, -0.5),
+ factors=((0.0, 0.01), (0.05, 0.0),),
+ duration_base=TimeType.from_float(base_time),
+ duration_factors=None
+ )
+ dependent_hold_1 = LinSpaceHold(
+ bases=(-1.0, -0.5),
+ factors=(None, (0.05,),),
+ duration_base=TimeType.from_float(base_time),
+ duration_factors=None
+ )
+ dependent_hold_2 = LinSpaceHold(
+ bases=(-0.5, -0.3),
+ factors=(None, (0.05,),),
+ duration_base=TimeType.from_float(base_time),
+ duration_factors=None
+ )
+
+ self.program = LinSpaceProgram((LinSpaceIter(
+ length=rep_factor,
+ body=(
+ dependent_hold_1,
+ LinSpaceRepeat(body=(dependent_hold_2,), count=rep_factor),
+ LinSpaceIter(body=(wait_hold,), length=rep_factor),
+ )
+ ),),
+ ('a','b'))
+
+ self.commands = [
+ Set(channel=0, value=-1.0, key=DepKey(factors=())),
+ Set(channel=1, value=-0.5, key=DepKey(factors=(50000000,))),
+ Wait(duration=TimeType(100, 1)),
+
+ Set(channel=0, value=-0.5, key=DepKey(factors=())),
+ Increment(channel=1, value=0.2, dependency_key=DepKey(factors=(50000000,))),
+ Wait(duration=TimeType(100, 1)),
+
+ # This is the repetition
+ LoopLabel(idx=0, count=1),
+ Wait(duration=TimeType(100, 1)),
+ LoopJmp(idx=0),
+
+ Set(channel=0, value=-1.0, key=DepKey(factors=(0, 10000000))),
+ Increment(channel=1, value=-0.2, dependency_key=DepKey(factors=(50000000,))),
+ Wait(duration=TimeType(100, 1)),
+
+ LoopLabel(idx=1, count=1),
+ Increment(channel=0, value=0.01, dependency_key=DepKey(factors=(0, 10000000))),
+ Wait(duration=TimeType(100, 1)),
+ LoopJmp(idx=1),
+
+ LoopLabel(idx=2, count=1),
+ Set(channel=0, value=-1.0, key=DepKey(factors=())),
+ Increment(channel=1, value=0.05, dependency_key=DepKey(factors=(50000000,))),
+ Wait(duration=TimeType(100, 1)),
+ Set(channel=0, value=-0.5, key=DepKey(factors=())),
+ Increment(channel=1, value=0.2, dependency_key=DepKey(factors=(50000000,))),
+ Wait(duration=TimeType(100, 1)),
+
+ # next repetition
+ LoopLabel(idx=3, count=1),
+ Wait(duration=TimeType(100, 1)),
+ LoopJmp(idx=3),
+
+ Increment(channel=0,
+ value=-0.01,
+ dependency_key=DepKey(factors=(0, 10000000))),
+ Increment(channel=1, value=-0.2, dependency_key=DepKey(factors=(50000000,))),
+ Wait(duration=TimeType(100, 1)),
+
+ LoopLabel(idx=4, count=1),
+ Increment(channel=0, value=0.01, dependency_key=DepKey(factors=(0, 10000000))),
+ Wait(duration=TimeType(100, 1)),
+ LoopJmp(idx=4),
+ LoopJmp(idx=2)]
+
+ time = TimeType(0)
+ self.output = []
+ for idx_b in range(rep_factor):
+ # does not account yet for floating poit errors. We would need to sum here
+ self.output.append((time, (-1.0, -0.5 + idx_b * 0.05)))
+ time += TimeType.from_float(base_time)
+
+ # The VM does not create noop entries
+ self.output.append((time, (-0.5, -0.3 + idx_b * 0.05)))
+ for _ in range(rep_factor):
+ time += TimeType.from_float(base_time)
+
+ for idx_a in range(rep_factor):
+ self.output.append((time, (-1.0 + 0.01 * idx_a, -0.5 + idx_b * 0.05)))
+ time += TimeType.from_float(base_time)
+
+ def test_program_1(self):
+ program_builder = LinSpaceBuilder(('a','b'))
+ program_1 = self.pulse_template.create_program(program_builder=program_builder)
+ self.assertEqual(self.program, program_1)
+
+ def test_commands_1(self):
+ commands = to_increment_commands(self.program)
+ self.assertEqual(self.commands, commands)
+
+ def test_output_1(self):
+ vm = LinSpaceVM(2)
+ vm.set_commands(commands=self.commands)
+ vm.run()
+ assert_vm_output_almost_equal(self, self.output, vm.history)
+
+
+class PrePostDepTest(TestCase):
+ def setUp(self):
+ hold = ConstantPT(10 ** 6, {'a': '-1. + idx * 0.01'})
+ hold_random = ConstantPT(10 ** 5, {'a': -.4})
+ # self.pulse_template = (hold_random@(hold_random@hold).with_repetition(10)@hold_random@hold)\
+ self.pulse_template = (hold_random @ hold.with_repetition(10)).with_iteration('idx', 200)
+
+ self.program = LinSpaceProgram((LinSpaceIter(
+ length=200,
+ body=(
+ LinSpaceHold(bases=(-.4,), factors=(None,), duration_base=TimeType(10**5), duration_factors=None),
+ LinSpaceRepeat(body=(
+ LinSpaceHold(bases=(-1.,),factors=((0.01,),),duration_base=TimeType(10**6),duration_factors=None),
+ ), count=10),
+ # LinSpaceHold(bases=(-.4),factors=None,duration_base=TimeType(10**6),duration_factors=None),
+ # LinSpaceHold(bases=(-1.,),factors=((0.01,),),duration_base=TimeType(10**6),duration_factors=None)
+ ),),),
+ ('a',)
+ )
+
+ self.commands = [
+ Set(channel=0, value=-0.4, key=DepKey(factors=())),
+ Wait(duration=TimeType(100000, 1)),
+ Set(channel=0, value=-1.0, key=DepKey(factors=(10000000,))),
+ Wait(duration=TimeType(1000000, 1)),
+ LoopLabel(idx=0, count=9),
+ Wait(duration=TimeType(1000000, 1)),
+ LoopJmp(idx=0),
+ LoopLabel(idx=1, count=199),
+ Set(channel=0, value=-0.4, key=DepKey(factors=())),
+ Wait(duration=TimeType(100000, 1)),
+ Increment(channel=0, value=0.01, dependency_key=DepKey(factors=(10000000,))),
+ Wait(duration=TimeType(1000000, 1)),
+ LoopLabel(idx=2, count=9),
+ Wait(duration=TimeType(1000000, 1)),
+ LoopJmp(idx=2),
+ LoopJmp(idx=1)
+ ]
+
+ self.output = []
+ time = TimeType(0)
+ for idx in range(200):
+ self.output.append((time, [-.4]))
+ time += TimeType(10**5)
+ self.output.append((time, [-1. + idx * 0.01]))
+ time += TimeType(10**7)
+
+
+ def test_program(self):
+ program_builder = LinSpaceBuilder(('a',))
+ program = self.pulse_template.create_program(program_builder=program_builder)
+ self.assertEqual(self.program, program)
+
+ def test_commands(self):
+ commands = to_increment_commands(self.program)
+ self.assertEqual(self.commands, commands)
+
+ def test_output(self):
+ vm = LinSpaceVM(1)
+ vm.set_commands(self.commands)
+ vm.run()
+ assert_vm_output_almost_equal(self, self.output, vm.history)
+
+
+class PlainCSDTest(TestCase):
+ def setUp(self):
+ hold = ConstantPT(10**6, {'a': '-1. + idx_a * 0.01', 'b': '-.5 + idx_b * 0.02'})
+ scan_a = hold.with_iteration('idx_a', 200)
+ self.pulse_template = scan_a.with_iteration('idx_b', 100)
+
+ self.program = LinSpaceProgram((LinSpaceIter(length=100, body=(LinSpaceIter(
+ length=200,
+ body=(LinSpaceHold(
+ bases=(-1., -0.5),
+ factors=((0.0, 0.01),
+ (0.02, 0.0)),
+ duration_base=TimeType(10**6),
+ duration_factors=None
+ ),)
+ ),)),), ('a', 'b'))
+
+ key_0 = DepKey.from_voltages((0, 0.01,), DEFAULT_INCREMENT_RESOLUTION)
+ key_1 = DepKey.from_voltages((0.02,), DEFAULT_INCREMENT_RESOLUTION)
+
+ self.commands = [
+ Set(0, -1.0, key_0),
+ Set(1, -0.5, key_1),
+ Wait(TimeType(10 ** 6)),
+
+ LoopLabel(0, 199),
+ Increment(0, 0.01, key_0),
+ Wait(TimeType(10 ** 6)),
+ LoopJmp(0),
+
+ LoopLabel(1, 99),
+
+ Increment(0, -1.99, key_0),
+ Increment(1, 0.02, key_1),
+ Wait(TimeType(10 ** 6)),
+
+ LoopLabel(2, 199),
+ Increment(0, 0.01, key_0),
+ Wait(TimeType(10 ** 6)),
+ LoopJmp(2),
+
+ LoopJmp(1),
+ ]
+
+ a_values = [sum([-1.] + [0.01] * i) for i in range(200)]
+ b_values = [sum([-.5] + [0.02] * j) for j in range(100)]
+
+ self.output = [
+ (
+ TimeType(10 ** 6 * (i + 200 * j)),
+ [a_values[i], b_values[j]]
+ ) for j in range(100) for i in range(200)
+ ]
+
+ def test_program(self):
+ program_builder = LinSpaceBuilder(('a', 'b'))
+ program = self.pulse_template.create_program(program_builder=program_builder)
+ self.assertEqual(self.program, program)
+
+ def test_increment_commands(self):
+ commands = to_increment_commands(self.program)
+ self.assertEqual(self.commands, commands)
+
+ def test_output(self):
+ vm = LinSpaceVM(2)
+ vm.set_commands(self.commands)
+ vm.run()
+ assert_vm_output_almost_equal(self, self.output, vm.history)
+
+
+class TiltedCSDTest(TestCase):
+ def setUp(self):
+ repetition_count = 3
+ hold = ConstantPT(10**6, {'a': '-1. + idx_a * 0.01 + idx_b * 1e-3', 'b': '-.5 + idx_b * 0.02 - 3e-3 * idx_a'})
+ scan_a = hold.with_iteration('idx_a', 200)
+ self.pulse_template = scan_a.with_iteration('idx_b', 100)
+ self.repeated_pt = self.pulse_template.with_repetition(repetition_count)
+
+ self.program = LinSpaceProgram((LinSpaceIter(length=100, body=(LinSpaceIter(
+ length=200,
+ body=(LinSpaceHold(
+ bases=(-1., -0.5),
+ factors=((1e-3, 0.01),
+ (0.02, -3e-3)),
+ duration_base=TimeType(10**6),
+ duration_factors=None
+ ),)
+ ),)),), ('a', 'b'))
+ self.repeated_program = LinSpaceProgram((LinSpaceRepeat(body=self.program.root, count=repetition_count),), ('a', 'b'))
+
+ key_0 = DepKey.from_voltages((1e-3, 0.01,), DEFAULT_INCREMENT_RESOLUTION)
+ key_1 = DepKey.from_voltages((0.02, -3e-3), DEFAULT_INCREMENT_RESOLUTION)
+
+ self.commands = [
+ Set(0, -1.0, key_0),
+ Set(1, -0.5, key_1),
+ Wait(TimeType(10 ** 6)),
+
+ LoopLabel(0, 199),
+ Increment(0, 0.01, key_0),
+ Increment(1, -3e-3, key_1),
+ Wait(TimeType(10 ** 6)),
+ LoopJmp(0),
+
+ LoopLabel(1, 99),
+
+ Increment(0, 1e-3 + -199 * 1e-2, key_0),
+ Increment(1, 0.02 + -199 * -3e-3, key_1),
+ Wait(TimeType(10 ** 6)),
+
+ LoopLabel(2, 199),
+ Increment(0, 0.01, key_0),
+ Increment(1, -3e-3, key_1),
+ Wait(TimeType(10 ** 6)),
+ LoopJmp(2),
+
+ LoopJmp(1),
+ ]
+ inner_commands = copy.deepcopy(self.commands)
+ for cmd in inner_commands:
+ if hasattr(cmd, 'idx'):
+ cmd.idx += 1
+ self.repeated_commands = [LoopLabel(0, repetition_count)] + inner_commands + [LoopJmp(0)]
+
+ self.output = [
+ (
+ TimeType(10 ** 6 * (i + 200 * j)),
+ [-1. + i * 0.01 + j * 1e-3, -.5 + j * 0.02 - 3e-3 * i]
+ ) for j in range(100) for i in range(200)
+ ]
+ self.repeated_output = [
+ (t + TimeType(10**6) * (n * 100 * 200), vals)
+ for n in range(repetition_count)
+ for t, vals in self.output
+ ]
+
+ def test_program(self):
+ program_builder = LinSpaceBuilder(('a', 'b'))
+ program = self.pulse_template.create_program(program_builder=program_builder)
+ self.assertEqual(self.program, program)
+
+ def test_repeated_program(self):
+ program_builder = LinSpaceBuilder(('a', 'b'))
+ program = self.repeated_pt.create_program(program_builder=program_builder)
+ self.assertEqual(self.repeated_program, program)
+
+ def test_increment_commands(self):
+ commands = to_increment_commands(self.program)
+ self.assertEqual(self.commands, commands)
+
+ def test_repeated_increment_commands(self):
+ commands = to_increment_commands(self.repeated_program)
+ self.assertEqual(self.repeated_commands, commands)
+
+ def test_output(self):
+ vm = LinSpaceVM(2)
+ vm.set_commands(self.commands)
+ vm.run()
+ assert_vm_output_almost_equal(self, self.output, vm.history)
+
+ def test_repeated_output(self):
+ vm = LinSpaceVM(2)
+ vm.set_commands(self.repeated_commands)
+ vm.run()
+ assert_vm_output_almost_equal(self, self.repeated_output, vm.history)
+
+
+class SingletLoadProcessing(TestCase):
+ def setUp(self):
+ wait = ConstantPT(10 ** 6, {'a': '-1. + idx_a * 0.01', 'b': '-.5 + idx_b * 0.02'})
+ load_random = ConstantPT(10 ** 5, {'a': -.4, 'b': -.3})
+ meas = ConstantPT(10 ** 5, {'a': 0.05, 'b': 0.06})
+
+ singlet_scan = (load_random @ wait @ meas).with_iteration('idx_a', 200).with_iteration('idx_b', 100)
+ self.pulse_template = singlet_scan
+
+ self.program = LinSpaceProgram((LinSpaceIter(length=100, body=(LinSpaceIter(
+ length=200,
+ body=(
+ LinSpaceHold(bases=(-0.4, -0.3), factors=(None, None), duration_base=TimeType(10 ** 5),
+ duration_factors=None),
+ LinSpaceHold(bases=(-1., -0.5),
+ factors=((0.0, 0.01),
+ (0.02, 0.0)),
+ duration_base=TimeType(10 ** 6),
+ duration_factors=None),
+ LinSpaceHold(bases=(0.05, 0.06), factors=(None, None), duration_base=TimeType(10 ** 5),
+ duration_factors=None),
+ )
+ ),)),), ('a', 'b'))
+
+ key_0 = DepKey.from_voltages((0, 0.01,), DEFAULT_INCREMENT_RESOLUTION)
+ key_1 = DepKey.from_voltages((0.02,), DEFAULT_INCREMENT_RESOLUTION)
+
+ self.commands = [
+ Set(0, -0.4),
+ Set(1, -0.3),
+ Wait(TimeType(10 ** 5)),
+ Set(0, -1.0, key_0),
+ Set(1, -0.5, key_1),
+ Wait(TimeType(10 ** 6)),
+ Set(0, 0.05),
+ Set(1, 0.06),
+ Wait(TimeType(10 ** 5)),
+
+ LoopLabel(0, 199),
+ Set(0, -0.4),
+ Set(1, -0.3),
+ Wait(TimeType(10 ** 5)),
+ Increment(0, 0.01, key_0),
+ Increment(1, 0.00, key_1),
+ Wait(TimeType(10 ** 6)),
+ Set(0, 0.05),
+ Set(1, 0.06),
+ Wait(TimeType(10 ** 5)),
+ LoopJmp(0),
+
+ LoopLabel(1, 99),
+
+ Set(0, -0.4),
+ Set(1, -0.3),
+ Wait(TimeType(10 ** 5)),
+ Increment(0, -1.99, key_0),
+ Increment(1, 0.02, key_1),
+ Wait(TimeType(10 ** 6)),
+ Set(0, 0.05),
+ Set(1, 0.06),
+ Wait(TimeType(10 ** 5)),
+
+ LoopLabel(2, 199),
+
+ Set(0, -0.4),
+ Set(1, -0.3),
+ Wait(TimeType(10 ** 5)),
+ Increment(0, 0.01, key_0),
+ Increment(1, 0.00, key_1),
+ Wait(TimeType(10 ** 6)),
+ Set(0, 0.05),
+ Set(1, 0.06),
+ Wait(TimeType(10 ** 5)),
+
+ LoopJmp(2),
+
+ LoopJmp(1),
+ ]
+
+ self.output = []
+ time = TimeType(0)
+ for idx_b in range(100):
+ for idx_a in range(200):
+ self.output.append(
+ (time, [-.4, -.3])
+ )
+ time += 10 ** 5
+ self.output.append(
+ (time, [-1. + idx_a * 0.01, -.5 + idx_b * 0.02])
+ )
+ time += 10 ** 6
+ self.output.append(
+ (time, [0.05, 0.06])
+ )
+ time += 10 ** 5
+
+ def test_singlet_scan_program(self):
+ program_builder = LinSpaceBuilder(('a', 'b'))
+ program = self.pulse_template.create_program(program_builder=program_builder)
+ self.assertEqual(self.program, program)
+
+ def test_singlet_scan_commands(self):
+ commands = to_increment_commands(self.program)
+ self.assertEqual(self.commands, commands)
+
+ def test_singlet_scan_output(self):
+ vm = LinSpaceVM(2)
+ vm.set_commands(self.commands)
+ vm.run()
+ assert_vm_output_almost_equal(self, self.output, vm.history)
+
+
+class TransformedRampTest(TestCase):
+ def setUp(self):
+ hold = ConstantPT(10 ** 6, {'a': '-1. + idx * 0.01'})
+ self.pulse_template = hold.with_iteration('idx', 200)
+ self.transformation = ScalingTransformation({'a': 2.0})
+
+ self.program = LinSpaceProgram((LinSpaceIter(
+ length=200,
+ body=(LinSpaceHold(
+ bases=(-2.,),
+ factors=((0.02,),),
+ duration_base=TimeType(10 ** 6),
+ duration_factors=None
+ ),)
+ ),), ('a',))
+
+ def test_global_trafo_program(self):
+ program_builder = LinSpaceBuilder(('a',))
+ program = self.pulse_template.create_program(program_builder=program_builder,
+ global_transformation=self.transformation)
+ self.assertEqual(self.program, program)
+
+ def test_local_trafo_program(self):
+ program_builder = LinSpaceBuilder(('a',))
+ with self.assertRaises(NotImplementedError):
+ # not implemented yet. This test should work as soon as its implemented
+ program = self.pulse_template.create_program(program_builder=program_builder,
+ global_transformation=self.transformation,
+ to_single_waveform={self.pulse_template})
+ self.assertEqual(self.program, program)
+
+
+class HarmonicPulseTest(TestCase):
+ def setUp(self):
+ hold_duration = TimeType(10 ** 6)
+ hold = ConstantPT(hold_duration, {'a': '-1. + idx * 0.01'})
+ sine = FunctionPulseTemplate('sin(2 * pi * t)', duration_expression='10 / pi', channel='a')
+ self.pulse_template = (hold @ sine).with_iteration('idx', 100)
+
+ self.sine_waveform = sine.build_waveform(parameters={}, channel_mapping={'a': 'a'})
+
+ self.program = LinSpaceProgram((LinSpaceIter(
+ length=100,
+ body=(LinSpaceHold(
+ bases=(-1.,),
+ factors=((0.01,),),
+ duration_base=hold_duration,
+ duration_factors=None
+ ),
+ LinSpaceArbitraryWaveform(
+ waveform=self.sine_waveform,
+ channels=('a',)
+ )
+ )
+ ),),('a',))
+
+ key = DepKey.from_voltages((0.01,), DEFAULT_INCREMENT_RESOLUTION)
+ self.commands = [
+ Set(0, -1.0, key),
+ Wait(hold_duration),
+ Play(self.sine_waveform, channels=('a',)),
+ LoopLabel(0, 99),
+ Increment(0, 0.01, key),
+ Wait(hold_duration),
+ Play(self.sine_waveform, channels=('a',)),
+ LoopJmp(0)
+ ]
+
+ self.sample_resolution = TimeType(1)
+ n_samples = int(math.ceil(self.sine_waveform.duration / self.sample_resolution))
+
+ step_duration = hold_duration + self.sine_waveform.duration
+
+ self.output = []
+ for idx in range(100):
+ hold_ampl = sum([-1.0] + [0.01] * idx)
+ self.output.append((idx * step_duration, [hold_ampl]))
+ for n in range(n_samples):
+ inner_time = n * self.sample_resolution
+ time = idx * step_duration + hold_duration + inner_time
+ value = np.sin(float(2 * np.pi * inner_time))
+ self.output.append((time, [value]))
+
+ def test_program(self):
+ program_builder = LinSpaceBuilder(('a',))
+ program = self.pulse_template.create_program(program_builder=program_builder)
+ self.assertEqual(self.program, program)
+
+ def test_commands(self):
+ commands = to_increment_commands(self.program)
+ self.assertEqual(self.commands, commands)
+
+ def test_output(self):
+ vm = LinSpaceVM(1, sample_resolution=self.sample_resolution)
+ vm.set_commands(self.commands)
+ vm.run()
+ assert_vm_output_almost_equal(self, self.output, vm.history)
diff --git a/tests/program/values_tests.py b/tests/program/values_tests.py
new file mode 100644
index 000000000..cb44a94fd
--- /dev/null
+++ b/tests/program/values_tests.py
@@ -0,0 +1,139 @@
+import copy
+import unittest
+from unittest import TestCase
+
+import numpy as np
+
+from qupulse.pulses import *
+from qupulse.program.linspace import *
+from qupulse.program.transformation import *
+from qupulse.pulses.function_pulse_template import FunctionPulseTemplate
+from qupulse.program.values import DynamicLinearValue, ResolutionDependentValue, DynamicLinearValueStepped
+from qupulse.utils.types import TimeType
+
+
+class DynamicLinearValueTests(TestCase):
+ def setUp(self):
+ self.d = DynamicLinearValue(-100,{'a':np.pi,'b':np.e})
+ self.d3 = DynamicLinearValue(-300,{'a':np.pi,'b':np.e})
+
+ def test_value(self):
+ dval = self.d.value({'a':12,'b':34})
+ np.testing.assert_allclose(dval, 12*np.pi+34*np.e-100)
+
+ # def test_abs(self):
+ # np.testing.assert_allclose(abs(self.d),100+np.pi+np.e)
+
+ def test_add_sub_neg(self):
+
+ self.assertEqual(self.d + 3,
+ DynamicLinearValue(-100+3,{'a':np.pi,'b':np.e}))
+ self.assertEqual(self.d + np.pi,
+ DynamicLinearValue(-100+np.pi,{'a':np.pi,'b':np.e}))
+ self.assertEqual(self.d + TimeType(12/5),
+ DynamicLinearValue(-100+TimeType(12/5),{'a':np.pi,'b':np.e}))
+ #sub
+ self.assertEqual(self.d - TimeType(12/5),
+ DynamicLinearValue(-100-TimeType(12/5),{'a':np.pi,'b':np.e}))
+
+ #this would raise because of TimeType conversion
+ # self.assertEqual(TimeType(12/5)-self.d,
+ # DynamicLinearValue(100+TimeType(12/5),{'a':-np.pi,'b':-np.e}))
+ #same type
+ self.assertEqual(self.d+DynamicLinearValue(0.1,{'b':1,'c':2}),
+ DynamicLinearValue(-99.9,{'a':np.pi,'b':np.e+1,'c':2}))
+
+ def test_mul(self):
+ self.assertEqual(self.d*3,
+ DynamicLinearValue(-3*100,{'a':3*np.pi,'b':3*np.e}))
+ self.assertEqual(3*self.d,
+ DynamicLinearValue(-3*100,{'a':3*np.pi,'b':3*np.e}))
+ #div
+ self.assertEqual(self.d3/3,
+ DynamicLinearValue(-100,{'a':np.pi/3,'b':np.e/3}))
+ #raise
+ self.assertRaises(TypeError,lambda: 3/self.d,)
+
+ def test_eq(self):
+
+ self.assertEqual(self.d==1,False)
+ self.assertEqual(self.d==1+1j,False)
+ # self.assertEqual(self.d>-101,True) #if one wants to allow these comparisons
+ # self.assertEqual(self.dself.d,False)
+ # self.assertEqual(self.d+1=self.d,True)
+ # self.assertEqual(self.d+1<=self.d,False)
+
+ # self.assertEqual(self.d>self.d/2-51,True)
+ # self.assertEqual(self.d None:
self.parameters = dict(t_duration=10, omega=3.14*2/10, t_y=3.4)
def test_scaling(self):
- from qupulse.pulses import plotting
+ from qupulse import plotting
parameters = {**self.parameters, 'foo': 5.3}
t_ref, reference, _ = plotting.render(self.complex_pt.create_program(parameters=parameters))
@@ -689,3 +678,19 @@ def test_offset(self):
_ = self.complex_pt - '4.5'
+class ArithmeticPulseTemplateSerializationTest(SerializableTests, unittest.TestCase):
+ def assert_equal_instance_except_id(self, lhs: ArithmeticPulseTemplate, rhs: ArithmeticPulseTemplate):
+ self.assertEqual(lhs.lhs, rhs.lhs)
+ self.assertEqual(lhs.rhs, rhs.rhs)
+ self.assertEqual(lhs._arithmetic_operator, rhs._arithmetic_operator)
+
+ @property
+ def class_to_test(self) -> typing.Any:
+ return ArithmeticPulseTemplate
+
+ def make_kwargs(self) -> dict:
+ return {
+ "lhs": 42.2,
+ "rhs": DummyPulseTemplate(),
+ "arithmetic_operator": "+",
+ }
diff --git a/tests/pulses/bug_tests.py b/tests/pulses/bug_tests.py
index ef4035e9f..44fd49677 100644
--- a/tests/pulses/bug_tests.py
+++ b/tests/pulses/bug_tests.py
@@ -11,9 +11,9 @@
from qupulse.pulses.mapping_pulse_template import MappingPulseTemplate
from qupulse.pulses.loop_pulse_template import ForLoopPulseTemplate
-from qupulse.pulses.plotting import plot
+from qupulse.plotting import plot
-from qupulse._program._loop import to_waveform
+from qupulse.program.loop import to_waveform
from qupulse.utils import isclose
class BugTests(unittest.TestCase):
@@ -77,7 +77,7 @@ def test_issue_584_uninitialized_table_sample(self):
"""issue 584"""
d = 598.3333333333334 - 480
tpt = TablePulseTemplate(entries={'P': [(0, 1.0, 'hold'), (d, 1.0, 'hold')]})
- with mock.patch('qupulse._program.waveforms.PULSE_TO_WAVEFORM_ERROR', 1e-6):
+ with mock.patch('qupulse.program.waveforms.PULSE_TO_WAVEFORM_ERROR', 1e-6):
wf = to_waveform(tpt.create_program())
self.assertTrue(isclose(d, wf.duration, abs_tol=1e-6))
diff --git a/tests/pulses/constant_pulse_template_tests.py b/tests/pulses/constant_pulse_template_tests.py
index ead1ce597..df33a527b 100644
--- a/tests/pulses/constant_pulse_template_tests.py
+++ b/tests/pulses/constant_pulse_template_tests.py
@@ -1,14 +1,15 @@
import unittest
+from unittest import mock
-import qupulse.pulses.plotting
-import qupulse._program.waveforms
+import qupulse.plotting
+import qupulse.program.waveforms
import qupulse.utils.sympy
from qupulse.pulses import TablePT, FunctionPT, AtomicMultiChannelPT, MappingPT
from qupulse.pulses.multi_channel_pulse_template import AtomicMultiChannelPulseTemplate
-from qupulse.pulses.plotting import plot
+from qupulse.plotting import plot
from qupulse.pulses.sequence_pulse_template import SequencePulseTemplate
-from qupulse._program._loop import make_compatible
-from qupulse._program.waveforms import ConstantWaveform
+from qupulse.program.loop import make_compatible
+from qupulse.program.waveforms import ConstantWaveform
from qupulse.serialization import DictBackend, PulseStorage
from qupulse.pulses.constant_pulse_template import ConstantPulseTemplate, ExpressionScalar, TimeType
@@ -35,19 +36,16 @@ def test_zero_duration(self):
p2 = ConstantPulseTemplate(0, {'P1': 1.})
p3 = ConstantPulseTemplate(2, {'P1': 1.})
- _ = qupulse.pulses.plotting.render(p1.create_program())
+ _ = qupulse.plotting.render(p1.create_program())
pulse = SequencePulseTemplate(p1, p2, p3)
prog = pulse.create_program()
- _ = qupulse.pulses.plotting.render(prog)
+ _ = qupulse.plotting.render(prog)
self.assertEqual(pulse.duration, 12)
def test_regression_duration_conversion(self):
- old_value = qupulse._program.waveforms.PULSE_TO_WAVEFORM_ERROR
-
- try:
- qupulse._program.waveforms.PULSE_TO_WAVEFORM_ERROR = 1e-6
+ with mock.patch("qupulse.program.waveforms.PULSE_TO_WAVEFORM_ERROR", 1e-6):
for duration_in_samples in [64, 936320, 24615392]:
p = ConstantPulseTemplate(duration_in_samples / 2.4, {'a': 0})
number_of_samples = p.create_program().duration * 2.4
@@ -56,33 +54,21 @@ def test_regression_duration_conversion(self):
p2 = ConstantPulseTemplate((duration_in_samples + 1) / 2.4, {'a': 0})
self.assertNotEqual(p.create_program().duration, p2.create_program().duration)
- finally:
- qupulse._program.waveforms.PULSE_TO_WAVEFORM_ERROR = old_value
def test_regression_duration_conversion_functionpt(self):
- old_value = qupulse._program.waveforms.PULSE_TO_WAVEFORM_ERROR
-
- try:
- qupulse._program.waveforms.PULSE_TO_WAVEFORM_ERROR = 1e-6
+ with mock.patch("qupulse.program.waveforms.PULSE_TO_WAVEFORM_ERROR", 1e-6):
for duration_in_samples in [64, 2000, 936320]:
p = FunctionPT('1', duration_expression=duration_in_samples / 2.4, channel='a')
number_of_samples = p.create_program().duration * 2.4
self.assertEqual(number_of_samples.denominator, 1)
- finally:
- qupulse._program.waveforms.PULSE_TO_WAVEFORM_ERROR = old_value
def test_regression_template_combination(self):
- old_value = qupulse.utils.sympy.SYMPY_DURATION_ERROR_MARGIN
-
- try:
- qupulse.utils.sympy.SYMPY_DURATION_ERROR_MARGIN = 1e-9
+ with mock.patch("qupulse.utils.sympy.SYMPY_DURATION_ERROR_MARGIN", 1e-9):
duration_in_seconds = 2e-6
full_template = ConstantPulseTemplate(duration=duration_in_seconds * 1e9, amplitude_dict={'C1': 1.1})
duration_in_seconds_derived = 1e-9 * full_template.duration
marker_pulse = TablePT({'marker': [(0, 0), (duration_in_seconds_derived * 1e9, 0)]})
full_template = AtomicMultiChannelPT(full_template, marker_pulse)
- finally:
- qupulse.utils.sympy.SYMPY_DURATION_ERROR_MARGIN = old_value
def test_regression_sequencept_with_mappingpt(self):
t1 = TablePT({'C1': [(0, 0), (100, 0)], 'C2': [(0, 1), (100, 1)]})
diff --git a/tests/pulses/loop_pulse_template_tests.py b/tests/pulses/loop_pulse_template_tests.py
index 0df6a2a04..b74d45f1e 100644
--- a/tests/pulses/loop_pulse_template_tests.py
+++ b/tests/pulses/loop_pulse_template_tests.py
@@ -10,7 +10,8 @@
from qupulse.pulses.parameters import InvalidParameterNameException, ParameterConstraintViolation,\
ParameterNotProvidedException, ParameterConstraint
-from qupulse._program._loop import Loop
+from qupulse.program.loop import LoopBuilder, Loop
+from qupulse.program.waveforms import SequenceWaveform
from tests.pulses.sequencing_dummies import DummyPulseTemplate, MeasurementWindowTestCase, DummyWaveform
from tests.serialization_dummies import DummySerializer
@@ -209,14 +210,11 @@ def test_create_program_constraint_on_loop_var_exception(self):
# loop index not accessible in current build_sequence -> Exception
children = [Loop(waveform=DummyWaveform(duration=2.0))]
program = Loop(children=children)
+ program_builder = LoopBuilder._testing_dummy([program])
+ program_builder.override(scope=scope)
with self.assertRaises(ParameterNotProvidedException):
- flt._internal_create_program(scope=scope,
- measurement_mapping=dict(),
- channel_mapping=dict(),
- parent_loop=program,
- to_single_waveform=set(),
- global_transformation=None)
+ flt._build_program(program_builder=program_builder)
self.assertEqual(children, list(program.children))
self.assertEqual(1, program.repetition_count)
self.assertIsNone(program._measurements)
@@ -234,13 +232,11 @@ def test_create_program_invalid_params(self) -> None:
children = [Loop(waveform=DummyWaveform(duration=2.0))]
program = Loop(children=children)
+ program_builder = LoopBuilder._testing_dummy([program])
+ program_builder.override(scope=invalid_scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping)
+
with self.assertRaises(ParameterConstraintViolation):
- flt._internal_create_program(scope=invalid_scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- parent_loop=program,
- to_single_waveform=set(),
- global_transformation=None)
+ flt._build_program(program_builder=program_builder)
self.assertEqual(children, list(program.children))
self.assertEqual(1, program.repetition_count)
@@ -259,13 +255,11 @@ def test_create_program_invalid_measurement_mapping(self) -> None:
children = [Loop(waveform=DummyWaveform(duration=2.0))]
program = Loop(children=children)
+ program_builder = LoopBuilder._testing_dummy([program])
+ program_builder.override(scope=invalid_scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping)
+
with self.assertRaises(KeyError):
- flt._internal_create_program(scope=invalid_scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- parent_loop=program,
- to_single_waveform=set(),
- global_transformation=None)
+ flt._build_program(program_builder=program_builder)
self.assertEqual(children, list(program.children))
self.assertEqual(1, program.repetition_count)
@@ -274,16 +268,12 @@ def test_create_program_invalid_measurement_mapping(self) -> None:
# test for broken mapping on child level. no guarantee that parent_loop is not changed, only check for exception
measurement_mapping = dict(A='B')
+ program_builder.override(measurement_mapping=measurement_mapping)
with self.assertRaises(KeyError):
- flt._internal_create_program(scope=invalid_scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- parent_loop=program,
- to_single_waveform=set(),
- global_transformation=None)
+ flt._build_program(program_builder=program_builder)
def test_create_program_missing_params(self) -> None:
- dt = DummyPulseTemplate(parameter_names={'i'}, waveform=DummyWaveform(duration=4.0), duration='t', measurements=[('b', 2, 1)])
+ dt = DummyPulseTemplate(parameter_names={'i'}, waveform=DummyWaveform(duration=4.0), measurements=[('b', 2, 1)])
flt = ForLoopPulseTemplate(body=dt, loop_index='i', loop_range=('a', 'b', 'c'),
measurements=[('A', 'alph', 1)], parameter_constraints=['c > 1'])
@@ -293,42 +283,25 @@ def test_create_program_missing_params(self) -> None:
children = [Loop(waveform=DummyWaveform(duration=2.0))]
program = Loop(children=children)
+ program_builder = LoopBuilder._testing_dummy([program])
+ program_builder.override(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping)
# test parameter in constraints
with self.assertRaises(ParameterNotProvidedException):
- flt._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- parent_loop=program,
- to_single_waveform=set(),
- global_transformation=None)
+ flt._build_program(program_builder=program_builder)
# test parameter in measurement mappings
scope = DictScope.from_kwargs(a=1, b=4, c=2)
+ program_builder.override(scope=scope)
with self.assertRaises(ParameterNotProvidedException):
- flt._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- parent_loop=program,
- to_single_waveform=set(),
- global_transformation=None)
-
- # test parameter in duration
- scope = DictScope.from_kwargs(a=1, b=4, c=2, alph=0)
- with self.assertRaises(ParameterNotProvidedException):
- flt._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- parent_loop=program,
- to_single_waveform=set(),
- global_transformation=None)
+ flt._build_program(program_builder=program_builder)
self.assertEqual(children, list(program.children))
self.assertEqual(1, program.repetition_count)
self.assertIsNone(program._measurements)
self.assert_measurement_windows_equal({}, program.get_measurement_windows())
- def test_create_program_body_none(self) -> None:
+ def test_build_program_body_none(self) -> None:
dt = DummyPulseTemplate(parameter_names={'i'}, waveform=None, duration=0,
measurements=[('b', 2, 1)])
flt = ForLoopPulseTemplate(body=dt, loop_index='i', loop_range=('a', 'b', 'c'),
@@ -338,19 +311,18 @@ def test_create_program_body_none(self) -> None:
measurement_mapping = dict(A='B', b='b')
channel_mapping = dict(C='D')
- program = Loop()
- flt._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- parent_loop=program,
- to_single_waveform=set(),
- global_transformation=None)
+ program_builder = LoopBuilder()
- self.assertEqual(0, len(program.children))
- self.assertEqual(1, program.repetition_count)
- self.assertEqual([], list(program.children))
+ program_builder.override(
+ scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping,
+ )
- def test_create_program(self) -> None:
+ flt._internal_build_program(program_builder=program_builder)
+
+ program = program_builder.to_program()
+ self.assertIsNone(program)
+
+ def test_build_program(self) -> None:
dt = DummyPulseTemplate(parameter_names={'i'},
waveform=DummyWaveform(duration=4.0, defined_channels={'A'}),
duration=4,
@@ -365,36 +337,21 @@ def test_create_program(self) -> None:
to_single_waveform = {'tom', 'jerry'}
global_transformation = TransformationStub()
- program = Loop()
+ program_builder = LoopBuilder()
+ program_builder.override(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping)
# inner _create_program does nothing
expected_program = Loop(measurements=[('B', .1, 1)])
- expected_create_program_kwargs = dict(measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- global_transformation=global_transformation,
- to_single_waveform=to_single_waveform,
- parent_loop=program)
- expected_create_program_calls = [mock.call(**expected_create_program_kwargs,
- scope=_ForLoopScope(scope, 'i', i))
+ expected_create_program_calls = [mock.call(program_builder=program_builder)
for i in (1, 3)]
- with mock.patch.object(flt, 'validate_scope') as validate_scope:
- with mock.patch.object(dt, '_create_program') as body_create_program:
- with mock.patch.object(flt, 'get_measurement_windows',
- wraps=flt.get_measurement_windows) as get_measurement_windows:
- flt._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- parent_loop=program,
- to_single_waveform=to_single_waveform,
- global_transformation=global_transformation)
-
- validate_scope.assert_called_once_with(scope=scope)
- get_measurement_windows.assert_called_once_with(scope, measurement_mapping)
- self.assertEqual(body_create_program.call_args_list, expected_create_program_calls)
-
- self.assertEqual(expected_program, program)
+ with mock.patch.object(dt, '_build_program') as body_create_program:
+ with mock.patch.object(flt, 'get_measurement_windows',
+ wraps=flt.get_measurement_windows) as get_measurement_windows:
+ flt._internal_build_program(program_builder=program_builder)
+ get_measurement_windows.assert_called_once_with(scope, measurement_mapping)
+ self.assertEqual(body_create_program.call_args_list, expected_create_program_calls)
def test_create_program_append(self) -> None:
dt = DummyPulseTemplate(parameter_names={'i'}, waveform=DummyWaveform(duration=4.0), duration=4,
@@ -408,12 +365,13 @@ def test_create_program_append(self) -> None:
children = [Loop(waveform=DummyWaveform(duration=2.0))]
program = Loop(children=children)
- flt._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- parent_loop=program,
- to_single_waveform=set(),
- global_transformation=None)
+
+ program_builder = LoopBuilder._testing_dummy([program])
+ program_builder.override(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping)
+
+ flt._internal_build_program(program_builder=program_builder)
+
+ program_builder.to_program()
self.assertEqual(3, len(program.children))
self.assertIs(children[0], program.children[0])
@@ -427,6 +385,15 @@ def test_create_program_append(self) -> None:
# not ensure same result as from Sequencer here - we're testing appending to an already existing parent loop
# which is a use case that does not immediately arise from using Sequencer
+ def test_single_waveform(self):
+ inner_wf = DummyWaveform()
+ inner_pt = DummyPulseTemplate(waveform=inner_wf, parameter_names={'idx'})
+
+ flpt = ForLoopPulseTemplate(inner_pt, loop_index='idx', loop_range=3, metadata=dict(to_single_waveform='always'))
+ program = flpt.create_program()
+ expected = Loop(children=[Loop(repetition_count=1, waveform=SequenceWaveform.from_sequence([inner_wf] * 3))])
+ self.assertEqual(expected, program)
+
class ForLoopPulseTemplateSerializationTests(SerializableTests, unittest.TestCase):
diff --git a/tests/pulses/mapping_pulse_template_tests.py b/tests/pulses/mapping_pulse_template_tests.py
index 366360d97..0d40f52b4 100644
--- a/tests/pulses/mapping_pulse_template_tests.py
+++ b/tests/pulses/mapping_pulse_template_tests.py
@@ -8,7 +8,8 @@
AmbiguousMappingException, MappingCollisionException
from qupulse.pulses.parameters import ParameterConstraintViolation, ParameterConstraint, ParameterNotProvidedException
from qupulse.expressions import Expression
-from qupulse._program._loop import Loop
+from qupulse.program import default_program_builder
+from qupulse.program.loop import Loop, LoopBuilder
from tests.pulses.sequencing_dummies import DummyPulseTemplate, MeasurementWindowTestCase, DummyWaveform
from tests.serialization_tests import SerializableTests
@@ -140,9 +141,6 @@ def test_from_tuple_partial_mappings(self):
measurement_mapping={'m1': 'n1'},
channel_mapping={'c1': 'd1'})
-
-
-
def test_external_params(self):
template = DummyPulseTemplate(parameter_names={'foo', 'bar'})
st = MappingPulseTemplate(template, parameter_mapping={'foo': 't*k', 'bar': 't*l'})
@@ -310,30 +308,17 @@ def test_create_program(self) -> None:
parameter_names={'t'})
st = MappingPulseTemplate(template, parameter_mapping=parameter_mapping,
measurement_mapping=measurement_mapping, channel_mapping=channel_mapping)
-
- pre_scope = DictScope.from_kwargs(k=5)
- pre_measurement_mapping = {'meas2': 'meas3'}
- pre_channel_mapping = {'default': 'A'}
-
- program = Loop()
- expected_inner_args = dict(scope=st.map_scope(pre_scope),
- measurement_mapping=st.get_updated_measurement_mapping(pre_measurement_mapping),
- channel_mapping=st.get_updated_channel_mapping(pre_channel_mapping),
- to_single_waveform=to_single_waveform,
- global_transformation=global_transformation,
- parent_loop=program)
-
- with mock.patch.object(template, '_create_program') as inner_create_program:
- st._internal_create_program(scope=pre_scope,
- measurement_mapping=pre_measurement_mapping,
- channel_mapping=pre_channel_mapping,
- to_single_waveform=to_single_waveform,
- global_transformation=global_transformation,
- parent_loop=program)
- inner_create_program.assert_called_once_with(**expected_inner_args)
-
- # as we mock the inner function there shouldnt be any changes
- self.assertEqual(program, Loop())
+ program_builder = mock.Mock()
+ program_builder.with_mappings = mock.MagicMock()
+
+ with mock.patch.object(template, '_build_program') as inner_build_program:
+ st._internal_build_program(program_builder=program_builder)
+ program_builder.with_mappings.assert_called_once_with(
+ parameter_mapping=parameter_mapping,
+ measurement_mapping=measurement_mapping,
+ channel_mapping=channel_mapping
+ )
+ inner_build_program.assert_called_once_with(program_builder=program_builder.with_mappings.return_value.__enter__.return_value)
def test_create_program_invalid_measurement_mapping(self) -> None:
measurement_mapping = {'meas1': 'meas2'}
@@ -352,14 +337,14 @@ def test_create_program_invalid_measurement_mapping(self) -> None:
pre_measurement_mapping = {}
pre_channel_mapping = {'default': 'A'}
- program = Loop()
+ program_builder = default_program_builder()
+ program_builder.override(
+ scope=pre_scope,
+ measurement_mapping=pre_measurement_mapping,
+ channel_mapping=pre_channel_mapping
+ )
with self.assertRaises(KeyError):
- st._internal_create_program(scope=pre_scope,
- measurement_mapping=pre_measurement_mapping,
- channel_mapping=pre_channel_mapping,
- to_single_waveform=set(),
- global_transformation=None,
- parent_loop=program)
+ st._build_program(program_builder=program_builder)
def test_create_program_parameter_constraint_violation(self) -> None:
measurement_mapping = {'meas1': 'meas2'}
@@ -379,14 +364,10 @@ def test_create_program_parameter_constraint_violation(self) -> None:
pre_measurement_mapping = {'meas2': 'meas3'}
pre_channel_mapping = {'default': 'A'}
- program = Loop()
+ program_builder = default_program_builder()
+ program_builder.override(scope=pre_scope, measurement_mapping=pre_measurement_mapping, channel_mapping=pre_channel_mapping)
with self.assertRaises(ParameterConstraintViolation):
- st._internal_create_program(scope=pre_scope,
- measurement_mapping=pre_measurement_mapping,
- channel_mapping=pre_channel_mapping,
- to_single_waveform=set(),
- global_transformation=None,
- parent_loop=program)
+ st._build_program(program_builder=program_builder)
def test_create_program_subtemplate_none(self) -> None:
measurement_mapping = {'meas1': 'meas2'}
@@ -406,24 +387,12 @@ def test_create_program_subtemplate_none(self) -> None:
pre_measurement_mapping = {'meas2': 'meas3'}
pre_channel_mapping = {'default': 'A'}
- program = Loop()
- st._internal_create_program(scope=pre_scope,
- measurement_mapping=pre_measurement_mapping,
- channel_mapping=pre_channel_mapping,
- to_single_waveform=set(),
- global_transformation=None,
- parent_loop=program)
-
- self.assertEqual(1, len(template.create_program_calls))
- self.assertEqual((st.map_scope(pre_scope),
- st.get_updated_measurement_mapping(pre_measurement_mapping),
- st.get_updated_channel_mapping(pre_channel_mapping),
- program),
- template.create_program_calls[-1])
-
- self.assertEqual(1, program.repetition_count)
- self.assertEqual(0, len(program.children))
- self.assertIsNone(program._measurements)
+ program_builder = LoopBuilder()
+ program_builder.override(scope=pre_scope, measurement_mapping=pre_measurement_mapping, channel_mapping=pre_channel_mapping)
+ st._internal_build_program(program_builder=program_builder)
+
+ template._internal_build_program.assert_called_once_with(program_builder)
+ self.assertIsNone(program_builder.to_program())
def test_same_channel_error(self):
@@ -432,6 +401,15 @@ def test_same_channel_error(self):
with self.assertRaisesRegex(ValueError, 'multiple channels to the same target'):
MappingPulseTemplate(dpt, channel_mapping={'A': 'X', 'B': 'X'})
+ def test_single_waveform(self):
+ inner_wf = DummyWaveform()
+ inner_pt = DummyPulseTemplate(waveform=inner_wf)
+
+ mpt = MappingPulseTemplate(inner_pt, metadata=dict(to_single_waveform='always'))
+ program = mpt.create_program()
+ expected = Loop(children=[Loop(repetition_count=1, waveform=inner_wf)])
+ self.assertEqual(expected, program)
+
class PulseTemplateParameterMappingExceptionsTests(unittest.TestCase):
diff --git a/tests/pulses/metadata_tests.py b/tests/pulses/metadata_tests.py
new file mode 100644
index 000000000..59762f7da
--- /dev/null
+++ b/tests/pulses/metadata_tests.py
@@ -0,0 +1,54 @@
+import unittest
+
+from qupulse.pulses.pulse_template import PulseTemplate, TemplateMetadata, MetadataComparison
+
+
+class MetadataTest(unittest.TestCase):
+ def test_default(self):
+ tm = TemplateMetadata()
+ self.assertIsNone(tm.to_single_waveform)
+
+ def test_overwrite(self):
+ tm = TemplateMetadata(to_single_waveform='always')
+ self.assertEqual("always", tm.to_single_waveform)
+
+ def test_custom_fields(self):
+ tm = TemplateMetadata(foo=42)
+ self.assertEqual(42, tm.foo)
+
+ tm.bar = 9
+ self.assertEqual(9, tm.bar)
+
+ def test_repr(self):
+ tm = TemplateMetadata()
+ self.assertEqual("TemplateMetadata()", repr(tm))
+
+ tm = TemplateMetadata(to_single_waveform='always')
+ self.assertEqual("TemplateMetadata(to_single_waveform='always')", repr(tm))
+
+ tm = TemplateMetadata(foo=42)
+ self.assertEqual("TemplateMetadata(foo=42)", repr(tm))
+
+ def test_serialization(self):
+ tm = TemplateMetadata()
+ self.assertEqual({}, tm.get_serialization_data())
+ # check double because this was a bug before due to a missing copy
+ self.assertEqual({}, tm.get_serialization_data())
+
+ tm = TemplateMetadata(to_single_waveform='always')
+ self.assertEqual({'to_single_waveform': 'always'}, tm.get_serialization_data())
+ self.assertEqual({'to_single_waveform': 'always'}, tm.get_serialization_data())
+
+ tm = TemplateMetadata(foo=42)
+ self.assertEqual({'foo': 42}, tm.get_serialization_data())
+ self.assertEqual({'foo': 42}, tm.get_serialization_data())
+
+ def test_bool(self):
+ tm = TemplateMetadata()
+ self.assertFalse(tm)
+
+ tm = TemplateMetadata(to_single_waveform='always')
+ self.assertTrue(tm)
+
+ tm = TemplateMetadata(foo=42)
+ self.assertTrue(tm)
diff --git a/tests/pulses/multi_channel_pulse_template_tests.py b/tests/pulses/multi_channel_pulse_template_tests.py
index 32cac0f72..d204c4208 100644
--- a/tests/pulses/multi_channel_pulse_template_tests.py
+++ b/tests/pulses/multi_channel_pulse_template_tests.py
@@ -6,14 +6,13 @@
from qupulse.parameter_scope import DictScope
from qupulse.pulses import RepetitionPT, ConstantPT
-from qupulse.pulses.plotting import render
-from qupulse.pulses.multi_channel_pulse_template import MultiChannelWaveform, MappingPulseTemplate,\
+from qupulse.plotting import render
+from qupulse.pulses.multi_channel_pulse_template import MappingPulseTemplate,\
ChannelMappingException, AtomicMultiChannelPulseTemplate, ParallelChannelPulseTemplate,\
TransformingWaveform, ParallelChannelTransformation
from qupulse.pulses.parameters import ParameterConstraint, ParameterConstraintViolation
-from qupulse.expressions import ExpressionScalar, Expression
+from qupulse.expressions import ExpressionScalar
from qupulse._program.transformation import LinearTransformation, chain_transformations
-from qupulse.utils.sympy import sympify
from tests.pulses.sequencing_dummies import DummyPulseTemplate, DummyWaveform
from tests.serialization_dummies import DummySerializer
@@ -415,7 +414,9 @@ def test_internal_create_program(self):
measurement_names={'M'}, waveform=DummyWaveform())
overwritten_channels = {'Y': 'c', 'Z': 'a', 'ToNone': 'foo'}
- parent_loop = object()
+ program_builder = mock.Mock()
+ program_builder.with_transformation = mock.MagicMock()
+
measurement_mapping = object()
channel_mapping = {'Y': 'O', 'Z': 'Z', 'X': 'X', 'ToNone': None}
to_single_waveform = object()
@@ -423,28 +424,21 @@ def test_internal_create_program(self):
other_kwargs = dict(measurement_mapping=measurement_mapping,
channel_mapping=channel_mapping,
to_single_waveform=to_single_waveform,
- parent_loop=parent_loop)
+ program_builder=program_builder)
pccpt = ParallelChannelPulseTemplate(template, overwritten_channels)
scope = DictScope.from_kwargs(c=1.2, a=3.4)
- kwargs = {**other_kwargs, 'scope': scope, 'global_transformation': None}
-
expected_overwritten_channels = {'O': 1.2, 'Z': 3.4}
expected_transformation = ParallelChannelTransformation(expected_overwritten_channels)
- expected_kwargs = {**kwargs, 'global_transformation': expected_transformation}
-
- with mock.patch.object(template, '_create_program', spec=template._create_program) as cp_mock:
- pccpt._internal_create_program(**kwargs)
- cp_mock.assert_called_once_with(**expected_kwargs)
- global_transformation = LinearTransformation(numpy.zeros((0, 0)), [], [])
- expected_transformation = chain_transformations(global_transformation, expected_transformation)
- kwargs = {**other_kwargs, 'scope': scope, 'global_transformation': global_transformation}
- expected_kwargs = {**kwargs, 'global_transformation': expected_transformation}
+ program_builder.build_context = mock.Mock()
+ program_builder.build_context.scope = scope
+ program_builder.build_context.channel_mapping = channel_mapping
- with mock.patch.object(template, '_create_program', spec=template._create_program) as cp_mock:
- pccpt._internal_create_program(**kwargs)
- cp_mock.assert_called_once_with(**expected_kwargs)
+ with mock.patch.object(template, '_build_program', spec=template._build_program) as cp_mock:
+ pccpt._internal_build_program(program_builder)
+ program_builder.with_transformation.assert_called_once_with(expected_transformation)
+ cp_mock.assert_called_once_with(program_builder=program_builder.with_transformation.return_value.__enter__.return_value)
def test_build_waveform(self):
template = DummyPulseTemplate(duration='t1', defined_channels={'X', 'Y'}, parameter_names={'a', 'b'},
diff --git a/tests/pulses/plotting_tests.py b/tests/pulses/plotting_tests.py
index 0abc56c85..ea3765c89 100644
--- a/tests/pulses/plotting_tests.py
+++ b/tests/pulses/plotting_tests.py
@@ -6,14 +6,20 @@
import numpy
from qupulse.pulses import ConstantPT
-from qupulse.pulses.plotting import PlottingNotPossibleException, render, plot
+from qupulse.plotting import PlottingNotPossibleException, render, plot
from qupulse.pulses.table_pulse_template import TablePulseTemplate
from qupulse.pulses.sequence_pulse_template import SequencePulseTemplate
-from qupulse._program._loop import Loop
+from qupulse.program.loop import Loop
from tests.pulses.sequencing_dummies import DummyWaveform, DummyPulseTemplate
+def use_svg_backend():
+ import matplotlib.pyplot
+ matplotlib.pyplot.close('all')
+ matplotlib.use('svg')
+
+
class PlotterTests(unittest.TestCase):
def test_render_loop_sliced(self) -> None:
wf = DummyWaveform(duration=19)
@@ -87,17 +93,18 @@ def integrated_test_with_sequencer_and_pulse_templates(self) -> None:
self.assertEqual(expected_voltages, voltages)
def test_plot_empty_pulse(self) -> None:
- import matplotlib
- matplotlib.use('svg') # use non-interactive backend so that test does not fail on travis
+ # use non-interactive backend so that test does not fail on travis
+ use_svg_backend()
pt = DummyPulseTemplate()
with self.assertWarnsRegex(UserWarning, "empty", msg="plot() did not issue a warning for an empty pulse"):
plot(pt, dict(), show=False)
def test_plot_pulse_automatic_sample_rate(self) -> None:
- import matplotlib
- matplotlib.use('svg') # use non-interactive backend so that test does not fail on travis
- pt=ConstantPT(100, {'a': 1})
+ # use non-interactive backend so that test does not fail on travis
+ use_svg_backend()
+
+ pt = ConstantPT(100, {'a': 1})
plot(pt, sample_rate=None)
def test_bug_447(self):
@@ -132,8 +139,8 @@ def test(self) -> None:
class PlottingIsinstanceTests(unittest.TestCase):
@unittest.skip("Breaks other tests")
def test_bug_422(self):
- import matplotlib
- matplotlib.use('svg') # use non-interactive backend so that test does not fail on travis
+ # use non-interactive backend so that test does not fail on travis
+ use_svg_backend()
to_reload = ['qupulse._program._loop',
'qupulse.pulses.pulse_template',
diff --git a/tests/pulses/pulse_template_parameter_mapping_tests.py b/tests/pulses/pulse_template_parameter_mapping_tests.py
index c88aeb15e..4ad3af34d 100644
--- a/tests/pulses/pulse_template_parameter_mapping_tests.py
+++ b/tests/pulses/pulse_template_parameter_mapping_tests.py
@@ -4,17 +4,3 @@
from qupulse.serialization import Serializer
from tests.pulses.sequencing_dummies import DummyPulseTemplate
from tests.serialization_dummies import DummyStorageBackend
-
-
-class TestPulseTemplateParameterMappingFileTests(unittest.TestCase):
-
- # ensure that a MappingPulseTemplate imported from pulse_template_parameter_mapping serializes as from mapping_pulse_template
- def test_pulse_template_parameter_include(self) -> None:
- with warnings.catch_warnings(record=True):
- warnings.simplefilter('ignore', DeprecationWarning)
- from qupulse.pulses.pulse_template_parameter_mapping import MappingPulseTemplate
- dummy_t = DummyPulseTemplate()
- map_t = MappingPulseTemplate(dummy_t)
- type_str = map_t.get_type_identifier()
- self.assertEqual("qupulse.pulses.mapping_pulse_template.MappingPulseTemplate", type_str)
-
diff --git a/tests/pulses/pulse_template_tests.py b/tests/pulses/pulse_template_tests.py
index d9919b10c..38df10799 100644
--- a/tests/pulses/pulse_template_tests.py
+++ b/tests/pulses/pulse_template_tests.py
@@ -2,17 +2,22 @@
import math
from unittest import mock
-from typing import Optional, Dict, Set, Any, Union
+from typing import Optional, Dict, Set, Any, Union, Sequence
+
+import frozendict
import sympy
from qupulse.parameter_scope import Scope, DictScope
+from qupulse.program.transformation import IdentityTransformation
+from qupulse.pulses.sequence_pulse_template import SequencePulseTemplate
from qupulse.utils.types import ChannelID
from qupulse.expressions import Expression, ExpressionScalar
from qupulse.pulses import ConstantPT, FunctionPT, RepetitionPT, ForLoopPT, ParallelChannelPT, MappingPT,\
- TimeReversalPT, AtomicMultiChannelPT
-from qupulse.pulses.pulse_template import AtomicPulseTemplate, PulseTemplate
+ TimeReversalPT, AtomicMultiChannelPT, SequencePT
+from qupulse.pulses.pulse_template import AtomicPulseTemplate, PulseTemplate, UnknownVolatileParameter
from qupulse.pulses.multi_channel_pulse_template import MultiChannelWaveform
-from qupulse._program._loop import Loop
+from qupulse.program.loop import Loop
+from qupulse.program import ProgramBuilder, default_program_builder
from qupulse._program.transformation import Transformation
from qupulse._program.waveforms import TransformingWaveform
@@ -20,15 +25,19 @@
from tests.pulses.sequencing_dummies import DummyWaveform
from tests._program.transformation_tests import TransformationStub
+from qupulse.program.loop import LoopBuilder
+
class PulseTemplateStub(PulseTemplate):
"""All abstract methods are stubs that raise NotImplementedError to catch unexpected calls. If a method is needed in
- a test one should use mock.patch or mock.patch.object"""
+ a test one should use mock.patch or mock.patch.object.
+ Properties can be passed as init argument because mocking them is a pita."""
def __init__(self, identifier=None,
defined_channels=None,
duration=None,
parameter_names=None,
measurement_names=None,
+ final_values=None,
registry=None):
super().__init__(identifier=identifier)
@@ -36,6 +45,7 @@ def __init__(self, identifier=None,
self._duration = duration
self._parameter_names = parameter_names
self._measurement_names = set() if measurement_names is None else measurement_names
+ self._final_values = final_values
self.internal_create_program_args = []
self._register(registry=registry)
@@ -72,7 +82,7 @@ def _internal_create_program(self, *,
channel_mapping: Dict[ChannelID, Optional[ChannelID]],
global_transformation: Optional[Transformation],
to_single_waveform: Set[Union[str, 'PulseTemplate']],
- parent_loop: Loop):
+ program_builder):
raise NotImplementedError()
@property
@@ -89,17 +99,33 @@ def initial_values(self) -> Dict[ChannelID, ExpressionScalar]:
@property
def final_values(self) -> Dict[ChannelID, ExpressionScalar]:
- raise NotImplementedError()
+ if self._final_values is None:
+ raise NotImplementedError()
+ else:
+ return self._final_values
+
+
+def get_appending_internal_build_program(waveform=DummyWaveform(),
+ always_append=False,
+ measurements: list=None):
+ def internal_create_program(program_builder: ProgramBuilder):
+ scope = program_builder.build_context.scope
+ if always_append or 'append_a_child' in scope:
+ if measurements is not None:
+ program_builder.measure(measurements=measurements)
+ program_builder.play_arbitrary_waveform(waveform=waveform)
+
+ return internal_create_program
def get_appending_internal_create_program(waveform=DummyWaveform(),
always_append=False,
measurements: list=None):
- def internal_create_program(*, scope, parent_loop: Loop, **_):
+ def internal_create_program(*, scope, program_builder: ProgramBuilder, **_):
if always_append or 'append_a_child' in scope:
if measurements is not None:
- parent_loop.add_measurements(measurements=measurements)
- parent_loop.append_child(waveform=waveform)
+ program_builder.measure(measurements=measurements)
+ program_builder.play_arbitrary_waveform(waveform=waveform)
return internal_create_program
@@ -164,7 +190,7 @@ def test_create_program(self) -> None:
expected_scope = DictScope.from_kwargs(foo=2.126, bar=-26.2, hugo=math.exp(math.sin(math.pi/2)),
volatile=volatile, append_a_child=1)
to_single_waveform = {'voll', 'toggo'}
- global_transformation = TransformationStub()
+ global_transformation = IdentityTransformation()
expected_internal_kwargs = dict(scope=expected_scope,
measurement_mapping=measurement_mapping,
@@ -175,16 +201,21 @@ def test_create_program(self) -> None:
dummy_waveform = DummyWaveform()
expected_program = Loop(children=[Loop(waveform=dummy_waveform)])
+ program_builder = LoopBuilder()
+
with mock.patch.object(template,
- '_create_program',
- wraps=get_appending_internal_create_program(dummy_waveform)) as _create_program:
- program = template.create_program(parameters=parameters,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- to_single_waveform=to_single_waveform,
- global_transformation=global_transformation,
- volatile=volatile)
- _create_program.assert_called_once_with(**expected_internal_kwargs, parent_loop=program)
+ '_build_program',
+ wraps=get_appending_internal_build_program(dummy_waveform)) as _build_program:
+ with mock.patch.object(program_builder, "override", wraps=program_builder.override) as override:
+ with mock.patch('qupulse.pulses.pulse_template.default_program_builder', return_value=program_builder):
+ program = template.create_program(parameters=parameters,
+ measurement_mapping=measurement_mapping,
+ channel_mapping=channel_mapping,
+ to_single_waveform=to_single_waveform,
+ global_transformation=global_transformation,
+ volatile=volatile)
+ override.assert_called_once_with(**expected_internal_kwargs)
+ _build_program.assert_called_once_with(program_builder=program_builder)
self.assertEqual(expected_program, program)
self.assertEqual(previos_measurement_mapping, measurement_mapping)
self.assertEqual(previous_channel_mapping, channel_mapping)
@@ -196,16 +227,17 @@ def test__create_program(self):
channel_mapping = {'B': 'A'}
global_transformation = TransformationStub()
to_single_waveform = {'voll', 'toggo'}
- parent_loop = Loop()
+ program_builder = LoopBuilder()
template = PulseTemplateStub()
with mock.patch.object(template, '_internal_create_program') as _internal_create_program:
- template._create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- global_transformation=global_transformation,
- to_single_waveform=to_single_waveform,
- parent_loop=parent_loop)
+ with self.assertWarns(DeprecationWarning):
+ template._create_program(scope=scope,
+ measurement_mapping=measurement_mapping,
+ channel_mapping=channel_mapping,
+ global_transformation=global_transformation,
+ to_single_waveform=to_single_waveform,
+ program_builder=program_builder)
_internal_create_program.assert_called_once_with(
scope=scope,
@@ -213,18 +245,19 @@ def test__create_program(self):
channel_mapping=channel_mapping,
global_transformation=global_transformation,
to_single_waveform=to_single_waveform,
- parent_loop=parent_loop)
+ program_builder=program_builder)
- self.assertEqual(parent_loop, Loop())
+ self.assertIsNone(program_builder.to_program())
with self.assertRaisesRegex(NotImplementedError, "volatile"):
template._parameter_names = {'c'}
- template._create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- global_transformation=global_transformation,
- to_single_waveform={template},
- parent_loop=parent_loop)
+ with self.assertWarns(DeprecationWarning):
+ template._create_program(scope=scope,
+ measurement_mapping=measurement_mapping,
+ channel_mapping=channel_mapping,
+ global_transformation=global_transformation,
+ to_single_waveform={template},
+ program_builder=program_builder)
def test__create_program_single_waveform(self):
template = PulseTemplateStub(identifier='pt_identifier', parameter_names={'alpha'})
@@ -234,7 +267,9 @@ def test__create_program_single_waveform(self):
scope = DictScope.from_kwargs(a=1., b=2., volatile={'a'})
measurement_mapping = {'M': 'N'}
channel_mapping = {'B': 'A'}
- parent_loop = Loop()
+
+ program_builder = LoopBuilder()
+ inner_program_builder = LoopBuilder()
wf = DummyWaveform()
single_waveform = DummyWaveform()
@@ -256,28 +291,31 @@ def test__create_program_single_waveform(self):
with mock.patch.object(template, '_internal_create_program',
wraps=appending_create_program) as _internal_create_program:
- with mock.patch('qupulse.pulses.pulse_template.to_waveform',
+ with mock.patch('qupulse.program.loop.to_waveform',
return_value=single_waveform) as to_waveform:
- template._create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- global_transformation=global_transformation,
- to_single_waveform=to_single_waveform,
- parent_loop=parent_loop)
+ with mock.patch('qupulse.program.loop.LoopBuilder', return_value=inner_program_builder):
+ with self.assertWarns(DeprecationWarning):
+ template._create_program(scope=scope,
+ measurement_mapping=measurement_mapping,
+ channel_mapping=channel_mapping,
+ global_transformation=global_transformation,
+ to_single_waveform=to_single_waveform,
+ program_builder=program_builder)
_internal_create_program.assert_called_once_with(scope=scope,
measurement_mapping=measurement_mapping,
channel_mapping=channel_mapping,
global_transformation=None,
to_single_waveform=to_single_waveform,
- parent_loop=expected_inner_program)
+ program_builder=inner_program_builder)
to_waveform.assert_called_once_with(expected_inner_program)
- expected_program._measurements = set(expected_program._measurements)
- parent_loop._measurements = set(parent_loop._measurements)
+ program = program_builder.to_program()
- self.assertEqual(expected_program, parent_loop)
+ expected_program._measurements = set(expected_program._measurements)
+ program._measurements = set(program._measurements)
+ self.assertEqual(expected_program, program)
def test_create_program_defaults(self) -> None:
template = PulseTemplateStub(defined_channels={'A', 'B'}, parameter_names={'foo'}, measurement_names={'hugo', 'foo'})
@@ -290,12 +328,15 @@ def test_create_program_defaults(self) -> None:
dummy_waveform = DummyWaveform()
expected_program = Loop(children=[Loop(waveform=dummy_waveform)])
+ program_builder = LoopBuilder()
with mock.patch.object(template,
'_internal_create_program',
wraps=get_appending_internal_create_program(dummy_waveform, True)) as _internal_create_program:
- program = template.create_program()
- _internal_create_program.assert_called_once_with(**expected_internal_kwargs, parent_loop=program)
+ with mock.patch('qupulse.pulses.pulse_template.default_program_builder', return_value=program_builder) as pb:
+ program = template.create_program()
+ pb.assert_called_once_with()
+ _internal_create_program.assert_called_once_with(**expected_internal_kwargs, program_builder=program_builder)
self.assertEqual(expected_program, program)
def test_create_program_channel_mapping(self):
@@ -307,12 +348,207 @@ def test_create_program_channel_mapping(self):
global_transformation=None,
to_single_waveform=set())
+ with mock.patch('qupulse.pulses.pulse_template.default_program_builder') as pb:
+ with mock.patch.object(template, '_build_program') as _build_program:
+ template.create_program(channel_mapping={'A': 'C'})
+ pb.assert_called_once_with()
+ pb.return_value.override.assert_called_once_with(**expected_internal_kwargs)
+ _build_program.assert_called_once_with(program_builder=pb.return_value)
+
+ def test_create_program_volatile(self):
+ template = PulseTemplateStub(defined_channels={'A', 'B'})
+
+ parameters = {'abc': 1.}
+
+ expected_internal_kwargs = dict(scope=DictScope.from_kwargs(volatile={'abc'}, **parameters),
+ measurement_mapping=dict(),
+ channel_mapping={'A': 'A', 'B': 'B'},
+ global_transformation=None,
+ to_single_waveform=set())
+
with mock.patch.object(template, '_internal_create_program') as _internal_create_program:
- template.create_program(channel_mapping={'A': 'C'})
+ program_builder = default_program_builder()
+ with mock.patch('qupulse.pulses.pulse_template.default_program_builder', return_value=program_builder):
+ template.create_program(parameters=parameters, volatile='abc')
- _internal_create_program.assert_called_once_with(**expected_internal_kwargs, parent_loop=Loop())
+ _internal_create_program.assert_called_once_with(**expected_internal_kwargs, program_builder=program_builder)
+ with mock.patch.object(template, '_internal_create_program') as _internal_create_program:
+ program_builder = default_program_builder()
+ with mock.patch('qupulse.pulses.pulse_template.default_program_builder', return_value=program_builder):
+ template.create_program(parameters=parameters, volatile={'abc'})
- def test_create_program_none(self) -> None:
+ _internal_create_program.assert_called_once_with(**expected_internal_kwargs, program_builder=program_builder)
+
+ expected_internal_kwargs = dict(scope=DictScope.from_kwargs(volatile={'abc', 'dfg'}, **parameters),
+ measurement_mapping=dict(),
+ channel_mapping={'A': 'A', 'B': 'B'},
+ global_transformation=None,
+ to_single_waveform=set())
+
+ program_builder = default_program_builder()
+ with mock.patch('qupulse.pulses.pulse_template.default_program_builder', return_value=program_builder):
+ with mock.patch.object(template, '_internal_create_program') as _internal_create_program:
+ with self.assertWarns(UnknownVolatileParameter):
+ template.create_program(parameters=parameters, volatile={'abc', 'dfg'})
+ _internal_create_program.assert_called_once_with(**expected_internal_kwargs, program_builder=program_builder)
+
+ def test_pad_to(self):
+ def to_multiple_of_192(x: Expression) -> Expression:
+ return (x + 191) // 192 * 192
+
+ final_values = frozendict.frozendict({'A': ExpressionScalar(0.1), 'B': ExpressionScalar('a')})
+ measurements = [('M', 0, 'y')]
+
+ pt = PulseTemplateStub(duration=ExpressionScalar(10))
+ padded = pt.pad_to(10)
+ self.assertIs(pt, padded)
+
+ pt = PulseTemplateStub(duration=ExpressionScalar('duration'))
+ padded = pt.pad_to('duration')
+ self.assertIs(pt, padded)
+
+ # padding with numeric durations
+
+ pt = PulseTemplateStub(duration=ExpressionScalar(10),
+ final_values=final_values,
+ defined_channels=final_values.keys())
+ padded = pt.pad_to(20)
+ self.assertEqual(padded.duration, 20)
+ self.assertEqual(padded.final_values, final_values)
+ self.assertIsInstance(padded, SequencePT)
+ self.assertIs(padded.subtemplates[0], pt)
+
+ with self.assertWarns(DeprecationWarning):
+ padded = pt.pad_to(20, pt_kwargs=dict(measurements=measurements))
+ self.assertEqual(padded.duration, 20)
+ self.assertEqual(padded.final_values, final_values)
+ self.assertIsInstance(padded, SequencePT)
+ self.assertIs(padded.subtemplates[0], pt)
+ self.assertEqual(measurements, padded.measurement_declarations)
+
+ with self.assertWarns(DeprecationWarning):
+ padded = pt.pad_to(10, pt_kwargs=dict(measurements=measurements))
+ self.assertEqual(padded.duration, 10)
+ self.assertEqual(padded.final_values, final_values)
+ self.assertIsInstance(padded, SequencePT)
+ self.assertIs(padded.subtemplates[0], pt)
+ self.assertEqual(measurements, padded.measurement_declarations)
+
+ # padding with numeric duation and callable
+ padded = pt.pad_to(to_multiple_of_192)
+ self.assertEqual(padded.duration, 192)
+ self.assertEqual(padded.final_values, final_values)
+ self.assertIsInstance(padded, SequencePT)
+ self.assertIs(padded.subtemplates[0], pt)
+
+ # padding with metadata
+ padded = pt.pad_to(to_multiple_of_192, spt_kwargs=dict(metadata={'to_single_waveform': 'always'}))
+ self.assertEqual(padded.duration, 192)
+ self.assertEqual(padded.final_values, final_values)
+ self.assertIsInstance(padded, SequencePT)
+ self.assertIs(padded.subtemplates[0], pt)
+ self.assertEqual(padded.metadata.get_serialization_data(), {'to_single_waveform': 'always'})
+
+ # padding with symbolic durations
+
+ pt = PulseTemplateStub(duration=ExpressionScalar('duration'),
+ final_values=final_values,
+ defined_channels=final_values.keys())
+ padded = pt.pad_to('new_duration')
+ self.assertEqual(padded.duration, 'new_duration')
+ self.assertEqual(padded.final_values, final_values)
+ self.assertIsInstance(padded, SequencePT)
+ self.assertIs(padded.subtemplates[0], pt)
+
+ # padding symbolic durations with callable
+
+ padded = pt.pad_to(to_multiple_of_192)
+ self.assertEqual(padded.duration, '(duration + 191) // 192 * 192')
+ self.assertEqual(padded.final_values, final_values)
+ self.assertIsInstance(padded, SequencePT)
+ self.assertIs(padded.subtemplates[0], pt)
+
+ # padding with metadata
+ padded = pt.pad_to(to_multiple_of_192, spt_kwargs=dict(metadata={'to_single_waveform': 'always'}))
+ self.assertEqual(padded.duration, '(duration + 191) // 192 * 192')
+ self.assertEqual(padded.final_values, final_values)
+ self.assertIsInstance(padded, SequencePT)
+ self.assertIs(padded.subtemplates[0], pt)
+ self.assertEqual(padded.metadata.get_serialization_data(), {'to_single_waveform': 'always'})
+
+
+ def test_pad_selected_subtemplates(self):
+ def to_multiple_of_192(x: Expression) -> Expression:
+ return (x + 191) // 192 * 192
+
+ final_values = frozendict.frozendict({'A': ExpressionScalar(0.1), 'B': ExpressionScalar('a')})
+
+ class DummyAPT(AtomicPulseTemplateStub):
+ @property
+ def final_values(self):
+ return final_values
+
+ @property
+ def defined_channels(self):
+ return final_values.keys()
+
+ def get_serialization_data(self, serializer=None) -> Dict[str, Any]:
+ assert not serializer
+ return {'duration': self.duration}
+
+
+ pt_10 = DummyAPT(duration=ExpressionScalar(10))
+ padded_10 = pt_10.pad_to(to_multiple_of_192)
+ padded_10_atomic = pt_10.pad_to(to_multiple_of_192, spt_kwargs=dict(metadata={'to_single_waveform': 'always'}))
+ pt_192 = DummyAPT(duration=ExpressionScalar(192))
+ pt_192_padded = pt_192.pad_to(to_multiple_of_192)
+ pt_192_padded_atomic = pt_192.pad_to(to_multiple_of_192, spt_kwargs=dict(metadata={'to_single_waveform': 'always'}))
+ pt_dyn = DummyAPT(duration=ExpressionScalar('duration'))
+ pt_dyn_padded = pt_dyn.pad_to(to_multiple_of_192)
+ pt_dyn_padded_atomic = pt_dyn.pad_to(to_multiple_of_192, spt_kwargs=dict(metadata={'to_single_waveform': 'always'}))
+ self.assertEqual(pt_dyn_padded, pt_dyn_padded_atomic)
+ self.assertEqual(padded_10, padded_10_atomic)
+ self.assertFalse(padded_10._is_atomic())
+ self.assertFalse(pt_dyn_padded._is_atomic())
+ self.assertTrue(padded_10_atomic._is_atomic())
+ self.assertTrue(pt_dyn_padded_atomic._is_atomic())
+ self.assertIs(pt_192_padded, pt_192)
+ self.assertIs(pt_192_padded_atomic, pt_192)
+
+ flat_spt = SequencePT(pt_10, pt_192, pt_dyn)
+
+ padded_flat = flat_spt.pad_selected_subtemplates_to(to_multiple_of_192)
+ expected = SequencePT(padded_10_atomic, pt_192, pt_dyn_padded_atomic)
+ self.assertEqual(expected, padded_flat)
+ for subpt in padded_flat.subtemplates:
+ self.assertTrue(subpt._is_atomic())
+
+ padded_flat_non_atomic = flat_spt.pad_selected_subtemplates_to(to_multiple_of_192, spt_kwargs=dict(metadata={}))
+ expected = SequencePT(padded_10, pt_192, pt_dyn_padded)
+ self.assertEqual(expected, padded_flat_non_atomic)
+ for expected_subpt, actual_subpt in zip(expected.subtemplates, padded_flat_non_atomic.subtemplates):
+ self.assertEqual(expected_subpt._is_atomic(), actual_subpt._is_atomic())
+
+ nested_pt = SequencePT(
+ pt_dyn,
+ RepetitionPT(pt_10, 2, 'rpt_10'),
+ RepetitionPT(pt_192, 2, 'rpt_192'),
+ pt_192,
+ )
+ nested_pt_padded = nested_pt.pad_selected_subtemplates_to(to_multiple_of_192)
+ expected = SequencePT(
+ pt_dyn_padded_atomic,
+ RepetitionPT(padded_10_atomic, 2, 'rpt_10_padded'),
+ RepetitionPT(pt_192, 2, 'rpt_192'),
+ pt_192,
+ )
+ self.assertEqual(expected, nested_pt_padded)
+ for subpt in nested_pt_padded.subtemplates:
+ expected_atomic = getattr(subpt, 'body', subpt)
+ self.assertTrue(expected_atomic._is_atomic())
+
+ @mock.patch('qupulse.pulses.pulse_template.default_program_builder')
+ def test_create_program_none(self, pb_mock) -> None:
template = PulseTemplateStub(defined_channels={'A'}, parameter_names={'foo'})
parameters = {'foo': 2.126, 'bar': -26.2, 'hugo': 'exp(sin(pi/2))'}
measurement_mapping = {'M': 'N'}
@@ -325,6 +561,7 @@ def test_create_program_none(self) -> None:
channel_mapping=channel_mapping,
global_transformation=None,
to_single_waveform=set())
+ pb_mock.return_value = LoopBuilder()
with mock.patch.object(template,
'_internal_create_program') as _internal_create_program:
@@ -332,7 +569,9 @@ def test_create_program_none(self) -> None:
measurement_mapping=measurement_mapping,
channel_mapping=channel_mapping,
volatile=volatile)
- _internal_create_program.assert_called_once_with(**expected_internal_kwargs, parent_loop=Loop())
+ pb_mock.assert_called_once_with()
+ _internal_create_program.assert_called_once_with(**expected_internal_kwargs,
+ program_builder=pb_mock.return_value)
self.assertIsNone(program)
def test_matmul(self):
@@ -344,6 +583,11 @@ def test_matmul(self):
self.assertEqual(a @ b, 'concat')
mock_concatenate.assert_called_once_with(a, b)
+ def test_pow(self):
+ pt = PulseTemplateStub()
+ pow_pt = pt ** 5
+ self.assertEqual(pow_pt, pt.with_repetition(5))
+
def test_rmatmul(self):
a = PulseTemplateStub()
b = (1, 2, 3)
@@ -363,8 +607,10 @@ def test_format(self):
class WithMethodTests(unittest.TestCase):
def setUp(self) -> None:
- self.fpt = FunctionPT(1.4, 'sin(f*t)', 'X')
+ self.fpt = FunctionPT(1.4, 'sin(f*t)', 'X', identifier='fpt')
self.cpt = ConstantPT(1.4, {'Y': 'start + idx * step'})
+ self.spt = SequencePT(self.cpt, RepetitionPT(self.cpt, 2, identifier='rpt'), identifier='spt')
+
def test_parallel_channels(self):
expected = ParallelChannelPT(self.fpt, {'K': 'k'})
actual = self.fpt.with_parallel_channels({'K': 'k'})
@@ -406,10 +652,71 @@ def test_parallel_atomic(self):
actual = self.fpt.with_parallel_atomic(self.cpt)
self.assertEqual(expected, actual)
+ def test_mapped_subtemplates(self):
+ expected = self.fpt
+ actual = self.fpt.with_mapped_subtemplates(map_fn=lambda x: 0/0)
+ self.assertEqual(expected, actual)
+
+ calls = []
+ def swap_c_and_f(pt):
+ calls.append(pt)
+ if pt == self.cpt:
+ return self.fpt
+ elif pt == self.fpt:
+ return self.cpt
+ else:
+ return pt
+
+ def identifier_map(identifier):
+ if identifier is None:
+ return None
+ else:
+ return identifier + '_mapped'
+
+ # PRE
+ expected_pre = SequencePT(self.fpt,
+ RepetitionPT(self.fpt, 2, identifier='rpt_mapped'),
+ identifier='spt_mapped')
+ expected_calls_pre = [
+ self.cpt,
+ self.cpt,
+ RepetitionPT(self.fpt, 2, 'rpt_mapped')
+ ]
+ actual_pre = self.spt.with_mapped_subtemplates(map_fn=swap_c_and_f,
+ recursion_strategy='pre',
+ identifier_map=identifier_map)
+ self.assertEqual(
+ expected_calls_pre,
+ calls,
+ )
+ self.assertEqual(expected_pre, actual_pre)
+
+ # POST
+ expected_post = SequencePT(self.fpt.renamed('fpt'),
+ RepetitionPT(self.fpt.renamed('fpt'), 2, identifier='rpt_mapped'),
+ identifier='spt_mapped')
+ expected_calls_post = [
+ self.cpt,
+ RepetitionPT(self.cpt, 2, identifier='rpt'),
+ self.cpt
+ ]
+ calls.clear()
+ actual_post = self.spt.with_mapped_subtemplates(map_fn=swap_c_and_f,
+ identifier_map=identifier_map,
+ recursion_strategy='post')
+ self.assertEqual(expected_calls_post, calls)
+ self.assertEqual(expected_post, actual_post)
+ calls.clear()
+
+ inner = RepetitionPT(self.fpt, 2, identifier='new_rpt')
+ expected_self = SequencePT(inner, inner, identifier='spt_mapped')
+ actual_self = self.spt.with_mapped_subtemplates(map_fn=lambda x: inner, recursion_strategy='self', identifier_map=identifier_map)
+ self.assertEqual(expected_self, actual_self)
+
class AtomicPulseTemplateTests(unittest.TestCase):
- def test_internal_create_program(self) -> None:
+ def test_internal_build_program(self) -> None:
measurement_windows = [('M', 0, 5)]
single_wf = DummyWaveform(duration=6, defined_channels={'A'})
wf = MultiChannelWaveform([single_wf])
@@ -418,82 +725,80 @@ def test_internal_create_program(self) -> None:
scope = DictScope.from_kwargs(foo=7.2, volatile={'gutes_zeuch'})
measurement_mapping = {'M': 'N'}
channel_mapping = {'B': 'A'}
- program = Loop()
+ program_builder = LoopBuilder()
expected_program = Loop(children=[Loop(waveform=wf)],
measurements=[('N', 0, 5)])
+ program_builder.override(
+ scope=scope,
+ measurement_mapping=measurement_mapping,
+ channel_mapping=channel_mapping,
+ global_transformation=None,
+ to_single_waveform=set()
+ )
with mock.patch.object(template, 'build_waveform', return_value=wf) as build_waveform:
- template._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- parent_loop=program,
- to_single_waveform=set(),
- global_transformation=None)
+ template._internal_build_program(program_builder=program_builder)
build_waveform.assert_called_once_with(parameters=scope, channel_mapping=channel_mapping)
-
+ program = program_builder.to_program()
self.assertEqual(expected_program, program)
- # MultiChannelProgram calls cleanup
- program.cleanup()
-
- def test_internal_create_program_transformation(self):
+ def test_internal_build_program_transformation(self):
inner_wf = DummyWaveform()
template = AtomicPulseTemplateStub(parameter_names=set())
- program = Loop()
+ program_builder = LoopBuilder()
global_transformation = TransformationStub()
scope = DictScope.from_kwargs()
expected_program = Loop(children=[Loop(waveform=TransformingWaveform(inner_wf, global_transformation))])
- with mock.patch.object(template, 'build_waveform', return_value=inner_wf):
- template._internal_create_program(scope=scope,
- measurement_mapping={},
- channel_mapping={},
- parent_loop=program,
- to_single_waveform=set(),
- global_transformation=global_transformation)
+ program_builder.override(scope=scope, global_transformation=global_transformation)
+ with mock.patch.object(template, 'build_waveform', return_value=inner_wf):
+ template._internal_build_program(program_builder=program_builder)
+ program = program_builder.to_program()
self.assertEqual(expected_program, program)
- def test_internal_create_program_no_waveform(self) -> None:
+ def test_internal_build_program_no_waveform(self) -> None:
measurement_windows = [('M', 0, 5)]
template = AtomicPulseTemplateStub(measurements=measurement_windows, parameter_names={'foo'})
scope = DictScope.from_kwargs(foo=3.5, bar=3, volatile={'bar'})
measurement_mapping = {'M': 'N'}
channel_mapping = {'B': 'A'}
- program = Loop()
-
- expected_program = Loop()
+ program_builder = LoopBuilder()
+ program_builder.override(
+ scope=scope,
+ measurement_mapping=measurement_mapping,
+ channel_mapping=channel_mapping,
+ global_transformation=None,
+ to_single_waveform=set()
+ )
with mock.patch.object(template, 'build_waveform', return_value=None) as build_waveform:
with mock.patch.object(template,
'get_measurement_windows',
wraps=template.get_measurement_windows) as get_meas_windows:
- template._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- parent_loop=program,
- to_single_waveform=set(),
- global_transformation=None)
+ template._internal_build_program(program_builder=program_builder)
build_waveform.assert_called_once_with(parameters=scope, channel_mapping=channel_mapping)
get_meas_windows.assert_not_called()
- self.assertEqual(expected_program, program)
+ self.assertIsNone(program_builder.to_program())
- def test_internal_create_program_volatile(self):
+ def test_internal_build_program_volatile(self):
template = AtomicPulseTemplateStub(parameter_names={'foo'})
scope = DictScope.from_kwargs(foo=3.5, bar=3, volatile={'foo'})
measurement_mapping = {'M': 'N'}
channel_mapping = {'B': 'A'}
- program = Loop()
+ program_builder = LoopBuilder()
+ program_builder.override(
+ scope=scope,
+ measurement_mapping=measurement_mapping,
+ channel_mapping=channel_mapping,
+ global_transformation=None,
+ to_single_waveform=set()
+ )
with self.assertRaisesRegex(AssertionError, "volatile"):
- template._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- parent_loop=program,
- to_single_waveform=set(),
- global_transformation=None)
- self.assertEqual(Loop(), program)
+ template._internal_build_program(program_builder=program_builder)
+ self.assertIsNone(program_builder.to_program())
diff --git a/tests/pulses/repetition_pulse_template_tests.py b/tests/pulses/repetition_pulse_template_tests.py
index b59cef820..8ee170e7e 100644
--- a/tests/pulses/repetition_pulse_template_tests.py
+++ b/tests/pulses/repetition_pulse_template_tests.py
@@ -3,9 +3,11 @@
from unittest import mock
from qupulse.parameter_scope import Scope, DictScope
+from qupulse.program.waveforms import RepetitionWaveform, TransformingWaveform
from qupulse.utils.types import FrozenDict
-from qupulse._program._loop import Loop
+from qupulse.program import default_program_builder
+from qupulse.program.loop import Loop, LoopBuilder
from qupulse.expressions import Expression, ExpressionScalar
from qupulse.pulses import ConstantPT
from qupulse.pulses.repetition_pulse_template import RepetitionPulseTemplate,ParameterNotIntegerException
@@ -15,7 +17,8 @@
from tests.serialization_dummies import DummySerializer
from tests.serialization_tests import SerializableTests
from tests._program.transformation_tests import TransformationStub
-from tests.pulses.pulse_template_tests import PulseTemplateStub, get_appending_internal_create_program
+from tests.pulses.pulse_template_tests import PulseTemplateStub, get_appending_internal_create_program, \
+ get_appending_internal_build_program
class RepetitionPulseTemplateTest(unittest.TestCase):
@@ -100,9 +103,24 @@ def test_parameter_names_param_only_in_constraint(self) -> None:
pt = RepetitionPulseTemplate(DummyPulseTemplate(parameter_names={'a'}), 'n', parameter_constraints=['a None:
+ program_builder.override(
+ scope=scope,
+ measurement_mapping=measurement_mapping,
+ channel_mapping=channel_mapping,
+ global_transformation=global_transformation,
+ to_single_waveform=to_single_waveform,
+ )
+ program_builder.with_repetition = mock.Mock(wraps=program_builder.with_repetition)
+
+ with mock.patch.object(body, '_build_program',
+ wraps=get_appending_internal_build_program(wf, always_append=True)) as body_build_program:
+ with mock.patch.object(rpt, 'get_repetition_count_value', return_value=6) as get_repetition_count_value:
+ with mock.patch.object(rpt, 'get_measurement_windows', return_value=[('l', .1, .2)]) as get_meas:
+ rpt._internal_build_program(program_builder=program_builder)
+ program = program_builder.to_program()
+
+ self.assertEqual(program, expected_program)
+ body_build_program.assert_called_once_with(program_builder=program_builder)
+ get_repetition_count_value.assert_called_once_with(scope)
+ get_meas.assert_called_once_with(scope, measurement_mapping)
+ program_builder.with_repetition.assert_called_once_with(6, measurements=[('l', .1, .2)])
+
+ def test_build_program_constant_success_measurements(self) -> None:
repetitions = 3
body = DummyPulseTemplate(duration=2.0, waveform=DummyWaveform(duration=2, defined_channels={'A'}), measurements=[('b', 0, 1)])
t = RepetitionPulseTemplate(body, repetitions, parameter_constraints=['foo<9'], measurements=[('my', 2, 2)])
scope = DictScope.from_mapping({'foo': 8})
measurement_mapping = {'my': 'thy', 'b': 'b'}
channel_mapping = {}
- program = Loop()
- t._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- to_single_waveform=set(),
- global_transformation=None,
- parent_loop=program)
+ program_builder = LoopBuilder()
+ program_builder.override(
+ scope=scope,
+ measurement_mapping=measurement_mapping,
+ channel_mapping=channel_mapping,
+ )
+
+ t._internal_build_program(program_builder=program_builder)
+
+ program = program_builder.to_program()
self.assertEqual(1, len(program.children))
internal_loop = program[0] # type: Loop
self.assertEqual(repetitions, internal_loop.repetition_count)
self.assertEqual(1, len(internal_loop))
- self.assertEqual((scope, measurement_mapping, channel_mapping, internal_loop), body.create_program_calls[-1])
self.assertEqual(body.waveform, internal_loop[0].waveform)
-
self.assert_measurement_windows_equal({'b': ([0, 2, 4], [1, 1, 1]), 'thy': ([2], [2])}, program.get_measurement_windows())
# done in MultiChannelProgram
@@ -178,38 +194,7 @@ def test_create_program_constant_success_measurements(self) -> None:
self.assert_measurement_windows_equal({'b': ([0, 2, 4], [1, 1, 1]), 'thy': ([2], [2])},
program.get_measurement_windows())
- def test_create_program_declaration_success(self) -> None:
- repetitions = "foo"
- body = DummyPulseTemplate(duration=2.0, waveform=DummyWaveform(duration=2, defined_channels={'A'}))
- t = RepetitionPulseTemplate(body, repetitions, parameter_constraints=['foo<9'])
- scope = DictScope.from_kwargs(foo=3)
- measurement_mapping = dict(moth='fire')
- channel_mapping = dict(asd='f')
- program = Loop()
- t._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- to_single_waveform=set(),
- global_transformation=None,
- parent_loop=program)
-
- self.assertEqual(1, program.repetition_count)
- self.assertEqual(1, len(program.children))
- internal_loop = program.children[0] # type: Loop
- self.assertEqual(scope[repetitions], internal_loop.repetition_count)
-
- self.assertEqual(1, len(internal_loop))
- self.assertEqual((scope, measurement_mapping, channel_mapping, internal_loop),
- body.create_program_calls[-1])
- self.assertEqual(body.waveform, internal_loop[0].waveform)
-
- self.assert_measurement_windows_equal({}, program.get_measurement_windows())
-
- # ensure same result as from Sequencer
- ## not the same as from Sequencer. Sequencer simplifies the whole thing to a single loop executing the waveform 3 times
- ## due to absence of non-repeated measurements. create_program currently does no such optimization
-
- def test_create_program_declaration_success_appended_measurements(self) -> None:
+ def test_build_program_declaration_success_appended_measurements(self) -> None:
repetitions = "foo"
body = DummyPulseTemplate(duration=2.0, waveform=DummyWaveform(duration=2), measurements=[('b', 0, 1)])
t = RepetitionPulseTemplate(body, repetitions, parameter_constraints=['foo<9'],
@@ -219,14 +204,10 @@ def test_create_program_declaration_success_appended_measurements(self) -> None:
channel_mapping = dict(asd='f')
children = [Loop(waveform=DummyWaveform(duration=0))]
program = Loop(children=children, measurements=[('a', [0], [1])], repetition_count=2)
+ program_builder = LoopBuilder._testing_dummy([program])
+ program_builder.override(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping)
- t._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- to_single_waveform=set(),
- global_transformation=None,
- parent_loop=program)
-
+ t._internal_build_program(program_builder=program_builder)
self.assertEqual(2, program.repetition_count)
self.assertEqual(2, len(program.children))
self.assertIs(program.children[0], children[0])
@@ -234,30 +215,22 @@ def test_create_program_declaration_success_appended_measurements(self) -> None:
self.assertEqual(scope[repetitions], internal_loop.repetition_count)
self.assertEqual(1, len(internal_loop))
- self.assertEqual((scope, measurement_mapping, channel_mapping, internal_loop), body.create_program_calls[-1])
self.assertEqual(body.waveform, internal_loop[0].waveform)
-
self.assert_measurement_windows_equal({'fire': ([0, 6], [7.1, 7.1]),
'b': ([0, 2, 4, 6, 8, 10], [1, 1, 1, 1, 1, 1]),
'a': ([0], [1])}, program.get_measurement_windows())
- # not ensure same result as from Sequencer here - we're testing appending to an already existing parent loop
- # which is a use case that does not immediately arise from using Sequencer
-
- def test_create_program_declaration_success_measurements(self) -> None:
+ def test_build_program_declaration_success_measurements(self) -> None:
repetitions = "foo"
body = DummyPulseTemplate(duration=2.0, waveform=DummyWaveform(duration=2), measurements=[('b', 0, 1)])
t = RepetitionPulseTemplate(body, repetitions, parameter_constraints=['foo<9'], measurements=[('moth', 0, 'meas_end')])
scope = DictScope.from_kwargs(foo=3, meas_end=7.1)
measurement_mapping = dict(moth='fire', b='b')
channel_mapping = dict(asd='f')
- program = Loop()
- t._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- to_single_waveform=set(),
- global_transformation=None,
- parent_loop=program)
+ program_builder = LoopBuilder()
+ program_builder.override(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping)
+ t._internal_build_program(program_builder=program_builder)
+ program = program_builder.to_program()
self.assertEqual(1, program.repetition_count)
self.assertEqual(1, len(program.children))
@@ -265,12 +238,11 @@ def test_create_program_declaration_success_measurements(self) -> None:
self.assertEqual(scope[repetitions], internal_loop.repetition_count)
self.assertEqual(1, len(internal_loop))
- self.assertEqual((scope, measurement_mapping, channel_mapping, internal_loop), body.create_program_calls[-1])
self.assertEqual(body.waveform, internal_loop[0].waveform)
self.assert_measurement_windows_equal({'fire': ([0], [7.1]), 'b': ([0, 2, 4], [1, 1, 1])}, program.get_measurement_windows())
- def test_create_program_declaration_exceeds_bounds(self) -> None:
+ def test_build_program_declaration_exceeds_bounds(self) -> None:
repetitions = "foo"
body_program = Loop(waveform=DummyWaveform(duration=1.0))
body = DummyPulseTemplate(duration=2.0, program=body_program)
@@ -281,20 +253,17 @@ def test_create_program_declaration_exceeds_bounds(self) -> None:
children = [Loop(waveform=DummyWaveform(duration=0))]
program = Loop(children=children)
+ program_builder = LoopBuilder._testing_dummy([program])
+ program_builder.override(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping)
with self.assertRaises(ParameterConstraintViolation):
- t._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- to_single_waveform=set(),
- global_transformation=None,
- parent_loop=program)
+ t._build_program(program_builder=program_builder)
self.assertFalse(body.create_program_calls)
self.assertEqual(1, program.repetition_count)
self.assertEqual(children, list(program.children))
self.assertIsNone(program.waveform)
self.assert_measurement_windows_equal({}, program.get_measurement_windows())
- def test_create_program_declaration_parameter_not_provided(self) -> None:
+ def test_build_program_declaration_parameter_not_provided(self) -> None:
repetitions = "foo"
body = DummyPulseTemplate(waveform=DummyWaveform(duration=2.0))
t = RepetitionPulseTemplate(body, repetitions, parameter_constraints=['foo<9'], measurements=[('a', 'd', 1)])
@@ -303,21 +272,10 @@ def test_create_program_declaration_parameter_not_provided(self) -> None:
channel_mapping = dict(asd='f')
children = [Loop(waveform=DummyWaveform(duration=0))]
program = Loop(children=children)
+ program_builder = LoopBuilder._testing_dummy([program])
+ program_builder.override(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping)
with self.assertRaises(ParameterNotProvidedException):
- t._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- to_single_waveform=set(),
- global_transformation=None,
- parent_loop=program)
-
- with self.assertRaises(ParameterNotProvidedException):
- t._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- to_single_waveform=set(),
- global_transformation=None,
- parent_loop=program)
+ t._internal_build_program(program_builder=program_builder)
self.assertFalse(body.create_program_calls)
self.assertEqual(1, program.repetition_count)
@@ -325,7 +283,7 @@ def test_create_program_declaration_parameter_not_provided(self) -> None:
self.assertIsNone(program.waveform)
self.assert_measurement_windows_equal({}, program.get_measurement_windows())
- def test_create_program_declaration_parameter_value_not_whole(self) -> None:
+ def test_build_program_declaration_parameter_value_not_whole(self) -> None:
repetitions = "foo"
body = DummyPulseTemplate(duration=2.0, waveform=DummyWaveform(duration=2.0))
t = RepetitionPulseTemplate(body, repetitions, parameter_constraints=['foo<9'])
@@ -334,20 +292,18 @@ def test_create_program_declaration_parameter_value_not_whole(self) -> None:
channel_mapping = dict(asd='f')
children = [Loop(waveform=DummyWaveform(duration=0))]
program = Loop(children=children)
+ program_builder = LoopBuilder._testing_dummy([program])
+ program_builder.override(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping)
+
with self.assertRaises(ParameterNotIntegerException):
- t._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- to_single_waveform=set(),
- global_transformation=None,
- parent_loop=program)
+ t._internal_build_program(program_builder=program_builder)
self.assertFalse(body.create_program_calls)
self.assertEqual(1, program.repetition_count)
self.assertEqual(children, list(program.children))
self.assertIsNone(program.waveform)
self.assert_measurement_windows_equal({}, program.get_measurement_windows())
- def test_create_program_constant_measurement_mapping_failure(self) -> None:
+ def test_build_program_constant_measurement_mapping_failure(self) -> None:
repetitions = "foo"
body = DummyPulseTemplate(duration=2.0, waveform=DummyWaveform(duration=2.0), measurements=[('b', 0, 1)])
t = RepetitionPulseTemplate(body, repetitions, parameter_constraints=['foo<9'], measurements=[('a', 0, 1)])
@@ -356,30 +312,27 @@ def test_create_program_constant_measurement_mapping_failure(self) -> None:
channel_mapping = dict(asd='f')
children = [Loop(waveform=DummyWaveform(duration=0))]
program = Loop(children=children)
+ program_builder = LoopBuilder._testing_dummy([program])
+ program_builder.override(
+ scope=scope,
+ channel_mapping=channel_mapping,
+ measurement_mapping=measurement_mapping,)
+
with self.assertRaises(KeyError):
- t._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- to_single_waveform=set(),
- global_transformation=None,
- parent_loop=program)
+ t._internal_build_program(program_builder=program_builder)
# test for failure on child level
measurement_mapping = dict(a='a')
+ program_builder.override(measurement_mapping=measurement_mapping)
with self.assertRaises(KeyError):
- t._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- to_single_waveform=set(),
- global_transformation=None,
- parent_loop=program)
+ t._internal_build_program(program_builder=program_builder)
self.assertFalse(body.create_program_calls)
self.assertEqual(1, program.repetition_count)
self.assertEqual(children, list(program.children))
self.assertIsNone(program.waveform)
self.assert_measurement_windows_equal({}, program.get_measurement_windows())
- def test_create_program_rep_count_zero_constant(self) -> None:
+ def test_build_program_rep_count_zero_constant(self) -> None:
repetitions = 0
body_program = Loop(waveform=DummyWaveform(duration=1.0))
body = DummyPulseTemplate(duration=2.0, program=body_program)
@@ -392,19 +345,14 @@ def test_create_program_rep_count_zero_constant(self) -> None:
measurement_mapping = dict(moth='fire')
channel_mapping = dict(asd='f')
- program = Loop()
- t._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- to_single_waveform=set(),
- global_transformation=None,
- parent_loop=program)
+ program_builder = LoopBuilder()
+ program_builder.override(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping)
+ t._internal_build_program(program_builder=program_builder)
+ program = program_builder.to_program()
self.assertFalse(body.create_program_calls)
- self.assertFalse(program.children)
- self.assertEqual(1, program.repetition_count)
- self.assertEqual(None, program._measurements)
+ self.assertIsNone(program)
- def test_create_program_rep_count_zero_constant_with_measurement(self) -> None:
+ def test_build_program_rep_count_zero_constant_with_measurement(self) -> None:
repetitions = 0
body_program = Loop(waveform=DummyWaveform(duration=1.0))
body = DummyPulseTemplate(duration=2.0, program=body_program)
@@ -417,19 +365,13 @@ def test_create_program_rep_count_zero_constant_with_measurement(self) -> None:
measurement_mapping = dict(moth='fire')
channel_mapping = dict(asd='f')
- program = Loop()
- t._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- to_single_waveform=set(),
- global_transformation=None,
- parent_loop=program)
- self.assertFalse(body.create_program_calls)
- self.assertFalse(program.children)
- self.assertEqual(1, program.repetition_count)
- self.assertEqual(None, program._measurements)
+ program_builder = default_program_builder()
+ program_builder.override(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping)
+ t._internal_build_program(program_builder=program_builder)
+ program = program_builder.to_program()
+ self.assertIsNone(program)
- def test_create_program_rep_count_zero_declaration(self) -> None:
+ def test_build_program_rep_count_zero_declaration(self) -> None:
repetitions = "foo"
body_program = Loop(waveform=DummyWaveform(duration=1.0))
body = DummyPulseTemplate(duration=2.0, program=body_program)
@@ -442,19 +384,14 @@ def test_create_program_rep_count_zero_declaration(self) -> None:
measurement_mapping = dict(moth='fire')
channel_mapping = dict(asd='f')
- program = Loop()
- t._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- to_single_waveform=set(),
- global_transformation=None,
- parent_loop=program)
+ program_builder = default_program_builder()
+ program_builder.override(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping)
+ t._internal_build_program(program_builder=program_builder)
+ program = program_builder.to_program()
self.assertFalse(body.create_program_calls)
- self.assertFalse(program.children)
- self.assertEqual(1, program.repetition_count)
- self.assertEqual(None, program._measurements)
+ self.assertIsNone(program)
- def test_create_program_rep_count_zero_declaration_with_measurement(self) -> None:
+ def test_build_program_rep_count_zero_declaration_with_measurement(self) -> None:
repetitions = "foo"
body_program = Loop(waveform=DummyWaveform(duration=1.0))
body = DummyPulseTemplate(duration=2.0, program=body_program)
@@ -467,24 +404,20 @@ def test_create_program_rep_count_zero_declaration_with_measurement(self) -> Non
measurement_mapping = dict(moth='fire')
channel_mapping = dict(asd='f')
- program = Loop()
- t._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- to_single_waveform=set(),
- global_transformation=None,
- parent_loop=program)
+ program_builder = default_program_builder()
+ program_builder.override(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping)
+ t._internal_build_program(program_builder=program_builder)
+ program = program_builder.to_program()
self.assertFalse(body.create_program_calls)
- self.assertFalse(program.children)
- self.assertEqual(1, program.repetition_count)
- self.assertEqual(None, program._measurements)
+ self.assertIsNone(program)
- def test_create_program_rep_count_neg_declaration(self) -> None:
+ def test_build_program_rep_count_neg_declaration(self) -> None:
repetitions = "foo"
body_program = Loop(waveform=DummyWaveform(duration=1.0))
body = DummyPulseTemplate(duration=2.0, program=body_program)
- # suppress warning about 0 repetitions on construction here, we are only interested in correct behavior during sequencing (i.e., do nothing)
+ # suppress warning about 0 repetitions on construction here.
+ # We are only interested in correct behavior during sequencing (i.e., do nothing)
with warnings.catch_warnings(record=True):
t = RepetitionPulseTemplate(body, repetitions)
@@ -492,19 +425,16 @@ def test_create_program_rep_count_neg_declaration(self) -> None:
measurement_mapping = dict(moth='fire')
channel_mapping = dict(asd='f')
- program = Loop()
- t._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- to_single_waveform=set(),
- global_transformation=None,
- parent_loop=program)
+ program_builder = default_program_builder()
+ program_builder.override(
+ scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping,
+ )
+ t._internal_build_program(program_builder=program_builder)
+ program = program_builder.to_program()
self.assertFalse(body.create_program_calls)
- self.assertFalse(program.children)
- self.assertEqual(1, program.repetition_count)
- self.assertEqual(None, program._measurements)
+ self.assertIsNone(program)
- def test_create_program_rep_count_neg_declaration_with_measurements(self) -> None:
+ def test_build_program_rep_count_neg_declaration_with_measurements(self) -> None:
repetitions = "foo"
body_program = Loop(waveform=DummyWaveform(duration=1.0))
body = DummyPulseTemplate(duration=2.0, program=body_program)
@@ -517,37 +447,26 @@ def test_create_program_rep_count_neg_declaration_with_measurements(self) -> Non
measurement_mapping = dict(moth='fire')
channel_mapping = dict(asd='f')
- program = Loop()
- t._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- to_single_waveform=set(),
- global_transformation=None,
- parent_loop=program)
+ program_builder = default_program_builder()
+ program_builder.override(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping)
+ t._internal_build_program(program_builder=program_builder)
+ program = program_builder.to_program()
self.assertFalse(body.create_program_calls)
- self.assertFalse(program.children)
- self.assertEqual(1, program.repetition_count)
- self.assertEqual(None, program._measurements)
+ self.assertIsNone(program)
- def test_create_program_none_subprogram(self) -> None:
+ def test_build_program_none_subprogram(self) -> None:
repetitions = "foo"
body = DummyPulseTemplate(duration=0.0, waveform=None)
t = RepetitionPulseTemplate(body, repetitions, parameter_constraints=['foo<9'])
scope = DictScope.from_kwargs(foo=3)
measurement_mapping = dict(moth='fire')
channel_mapping = dict(asd='f')
- program = Loop()
- t._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- to_single_waveform=set(),
- global_transformation=None,
- parent_loop=program)
- self.assertFalse(program.children)
- self.assertEqual(1, program.repetition_count)
- self.assertEqual(None, program._measurements)
+ program_builder = default_program_builder()
+ program_builder.override(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping)
+ t._internal_build_program(program_builder=program_builder)
+ self.assertIsNone(program_builder.to_program())
- def test_create_program_none_subprogram_with_measurement(self) -> None:
+ def test_build_program_none_subprogram_with_measurement(self) -> None:
repetitions = "foo"
body = DummyPulseTemplate(duration=2.0, waveform=None, measurements=[('b', 2, 3)])
t = RepetitionPulseTemplate(body, repetitions, parameter_constraints=['foo<9'],
@@ -555,17 +474,20 @@ def test_create_program_none_subprogram_with_measurement(self) -> None:
scope = DictScope.from_kwargs(foo=3, meas_end=7.1)
measurement_mapping = dict(moth='fire', b='b')
channel_mapping = dict(asd='f')
- program = Loop()
-
- t._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- to_single_waveform=set(),
- global_transformation=None,
- parent_loop=program)
- self.assertFalse(program.children)
- self.assertEqual(1, program.repetition_count)
- self.assertEqual(None, program._measurements)
+ program_builder = default_program_builder()
+ program_builder.override(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping)
+
+ t._internal_build_program(program_builder=program_builder)
+ self.assertIsNone(program_builder.to_program())
+
+ def test_single_waveform(self):
+ inner_wf = DummyWaveform()
+ inner_pt = DummyPulseTemplate(waveform=inner_wf)
+
+ rpt = RepetitionPulseTemplate(inner_pt, repetition_count=42, metadata=dict(to_single_waveform='always'))
+ program = rpt.create_program()
+ expected = Loop(children=[Loop(repetition_count=1, waveform=RepetitionWaveform.from_repetition_count(inner_wf, 42))])
+ self.assertEqual(expected, program)
class RepetitionPulseTemplateSerializationTests(SerializableTests, unittest.TestCase):
diff --git a/tests/pulses/sequence_pulse_template_tests.py b/tests/pulses/sequence_pulse_template_tests.py
index 4985fb8de..24791bdc1 100644
--- a/tests/pulses/sequence_pulse_template_tests.py
+++ b/tests/pulses/sequence_pulse_template_tests.py
@@ -3,17 +3,20 @@
from qupulse.parameter_scope import DictScope
from qupulse.expressions import Expression, ExpressionScalar
+from qupulse.program.waveforms import TransformingWaveform
from qupulse.pulses.table_pulse_template import TablePulseTemplate
from qupulse.pulses.sequence_pulse_template import SequencePulseTemplate, SequenceWaveform
from qupulse.pulses.mapping_pulse_template import MappingPulseTemplate
from qupulse.pulses.parameters import ParameterConstraint, ParameterConstraintViolation, ParameterNotProvidedException
-from qupulse._program._loop import Loop
+from qupulse.program import default_program_builder
+from qupulse.program.loop import LoopBuilder, Loop
from tests.pulses.sequencing_dummies import DummyPulseTemplate, DummyWaveform, MeasurementWindowTestCase
from tests.serialization_dummies import DummySerializer
from tests.serialization_tests import SerializableTests
from tests._program.transformation_tests import TransformationStub
-from tests.pulses.pulse_template_tests import get_appending_internal_create_program, PulseTemplateStub
+from tests.pulses.pulse_template_tests import get_appending_internal_create_program, PulseTemplateStub, \
+ get_appending_internal_build_program
class SequencePulseTemplateTest(unittest.TestCase):
@@ -75,9 +78,18 @@ def test_build_waveform(self):
self.assertIs(pt.build_waveform_calls[0][0], parameters)
self.assertIsInstance(wf, SequenceWaveform)
- for wfa, wfb in zip(wf.compare_key, wfs):
+ for wfa, wfb in zip(wf.sequenced_waveforms, wfs):
self.assertIs(wfa, wfb)
+ def test_single_waveform(self):
+ wfs = [DummyWaveform(), DummyWaveform()]
+ pts = [DummyPulseTemplate(waveform=wf) for wf in wfs]
+
+ spt = SequencePulseTemplate(*pts, metadata=dict(to_single_waveform='always'))
+ program = spt.create_program()
+ expected = Loop(children=[Loop(repetition_count=1, waveform=SequenceWaveform.from_sequence(wfs))])
+ self.assertEqual(expected, program)
+
def test_identifier(self) -> None:
identifier = 'some name'
pulse = SequencePulseTemplate(DummyPulseTemplate(), identifier=identifier)
@@ -124,6 +136,7 @@ def test_concatenate(self):
spt_id = SequencePulseTemplate(a, b, identifier='id')
spt_meas = SequencePulseTemplate(a, b, measurements=[('m', 0, 'd')])
spt_constr = SequencePulseTemplate(a, b, parameter_constraints=['a < b'])
+ spt_metadata = SequencePulseTemplate(a, b, metadata={'to_single_waveform': 'always'})
merged = SequencePulseTemplate.concatenate(a, spt_anon, b)
self.assertEqual(merged.subtemplates, [a, a, b, b])
@@ -137,6 +150,9 @@ def test_concatenate(self):
result = SequencePulseTemplate.concatenate(a, spt_constr, b)
self.assertEqual(result.subtemplates, [a, spt_constr, b])
+ result = SequencePulseTemplate.concatenate(a, spt_metadata, b)
+ self.assertEqual(result.subtemplates, [a, spt_metadata, b])
+
class SequencePulseTemplateSerializationTests(SerializableTests, unittest.TestCase):
@@ -220,7 +236,7 @@ def test_deserialize_old(self) -> None:
class SequencePulseTemplateSequencingTests(MeasurementWindowTestCase):
- def test_internal_create_program(self):
+ def test_build_program(self):
sub_templates = PulseTemplateStub(defined_channels={'a'}, duration=ExpressionScalar('t1')),\
PulseTemplateStub(defined_channels={'a'}, duration=ExpressionScalar('t2'))
@@ -236,44 +252,46 @@ def test_internal_create_program(self):
channel_mapping={'g': 'h'},
global_transformation=TransformationStub(),
to_single_waveform={'to', 'single', 'waveform'})
+ expected_wfs = TransformingWaveform(wfs[0], kwargs['global_transformation']), TransformingWaveform(wfs[1], kwargs['global_transformation'])
- program = Loop()
+ program_builder = LoopBuilder()
+ program_builder.override(**kwargs)
- expected_program = Loop(children=[Loop(waveform=wfs[0]),
- Loop(waveform=wfs[1])],
+ expected_program = Loop(children=[Loop(waveform=expected_wfs[0]),
+ Loop(waveform=expected_wfs[1])],
measurements=[('l', .1, .2)])
with mock.patch.object(spt, 'validate_scope') as validate_scope:
with mock.patch.object(spt, 'get_measurement_windows',
return_value=[('l', .1, .2)]) as get_measurement_windows:
- with mock.patch.object(sub_templates[0], '_create_program',
- wraps=get_appending_internal_create_program(wfs[0], True)) as create_0,\
- mock.patch.object(sub_templates[1], '_create_program',
- wraps=get_appending_internal_create_program(wfs[1], True)) as create_1:
+ with mock.patch.object(sub_templates[0], '_build_program',
+ wraps=get_appending_internal_build_program(wfs[0], True)) as create_0,\
+ mock.patch.object(sub_templates[1], '_build_program',
+ wraps=get_appending_internal_build_program(wfs[1], True)) as create_1:
+
+ spt._build_program(program_builder=program_builder)
- spt._internal_create_program(**kwargs, parent_loop=program)
+ program = program_builder.to_program()
self.assertEqual(expected_program, program)
validate_scope.assert_called_once_with(kwargs['scope'])
get_measurement_windows.assert_called_once_with(kwargs['scope'], kwargs['measurement_mapping'])
- create_0.assert_called_once_with(**kwargs, parent_loop=program)
- create_1.assert_called_once_with(**kwargs, parent_loop=program)
+ create_0.assert_called_once_with(program_builder=program_builder)
+ create_1.assert_called_once_with(program_builder=program_builder)
- def test_create_program_internal(self) -> None:
+ def test_internal_build_program_internal(self) -> None:
sub1 = DummyPulseTemplate(duration=3, waveform=DummyWaveform(duration=3), measurements=[('b', 1, 2)], defined_channels={'A'})
sub2 = DummyPulseTemplate(duration=2, waveform=DummyWaveform(duration=2), parameter_names={'foo'}, defined_channels={'A'})
scope = DictScope.from_kwargs()
measurement_mapping = {'a': 'a', 'b': 'b'}
channel_mapping = dict()
seq = SequencePulseTemplate(sub1, sub2, measurements=[('a', 0, 1)])
- loop = Loop()
- seq._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- global_transformation=None,
- to_single_waveform=set(),
- parent_loop=loop)
+ program_builder = LoopBuilder()
+ program_builder.override(measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, scope=scope)
+
+ seq._internal_build_program(program_builder=program_builder)
+ loop = program_builder.to_program()
self.assertEqual(1, loop.repetition_count)
self.assertIsNone(loop.waveform)
self.assertEqual([Loop(repetition_count=1, waveform=sub1.waveform),
@@ -283,13 +301,10 @@ def test_create_program_internal(self) -> None:
### test again with inverted sequence
seq = SequencePulseTemplate(sub2, sub1, measurements=[('a', 0, 1)])
- loop = Loop()
- seq._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- global_transformation=None,
- to_single_waveform=set(),
- parent_loop=loop)
+ program_builder = LoopBuilder()
+ program_builder.override(measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, scope=scope)
+ seq._internal_build_program(program_builder=program_builder)
+ loop = program_builder.to_program()
self.assertEqual(1, loop.repetition_count)
self.assertIsNone(loop.waveform)
self.assertEqual([Loop(repetition_count=1, waveform=sub2.waveform),
@@ -297,23 +312,18 @@ def test_create_program_internal(self) -> None:
list(loop.children))
self.assert_measurement_windows_equal({'a': ([0], [1]), 'b': ([3], [2])}, loop.get_measurement_windows())
- def test_internal_create_program_no_measurement_mapping(self) -> None:
+ def test_internal_build_program_no_measurement_mapping(self) -> None:
sub1 = DummyPulseTemplate(duration=3, waveform=DummyWaveform(duration=3), measurements=[('b', 1, 2)])
sub2 = DummyPulseTemplate(duration=2, waveform=DummyWaveform(duration=2), parameter_names={'foo'})
scope = DictScope.from_kwargs()
seq = SequencePulseTemplate(sub1, sub2, measurements=[('a', 0, 1)])
children = [Loop(waveform=DummyWaveform())]
loop = Loop(measurements=[], children=children)
+ program_builder = LoopBuilder._testing_dummy([loop])
+ program_builder.override(scope=scope, measurement_mapping={})
with self.assertRaises(KeyError):
- seq._internal_create_program(scope=scope,
- measurement_mapping=dict(),
- channel_mapping=dict(),
- global_transformation=None,
- to_single_waveform=set(),
-
- parent_loop=loop)
-
+ seq._build_program(program_builder=program_builder)
self.assertFalse(sub1.create_program_calls)
self.assertFalse(sub2.create_program_calls)
self.assertEqual(children, list(loop.children))
@@ -322,29 +332,24 @@ def test_internal_create_program_no_measurement_mapping(self) -> None:
self.assert_measurement_windows_equal({}, loop.get_measurement_windows())
# test for child level measurements (does not guarantee to leave parent_loop unchanged in this case)
+ program_builder.override(
+ scope=scope,
+ measurement_mapping=dict(a='a'),
+ )
with self.assertRaises(KeyError):
- seq._internal_create_program(scope=scope,
- measurement_mapping=dict(a='a'),
- channel_mapping=dict(),
- global_transformation=None,
- to_single_waveform=set(),
-
- parent_loop=loop)
+ seq._internal_build_program(program_builder=program_builder)
- def test_internal_create_program_one_child_no_duration(self) -> None:
+ def test_internal_build_program_one_child_no_duration(self) -> None:
sub1 = DummyPulseTemplate(duration=0, waveform=None, measurements=[('b', 1, 2)], defined_channels={'A'})
sub2 = DummyPulseTemplate(duration=2, waveform=DummyWaveform(duration=2), parameter_names={'foo'}, defined_channels={'A'})
scope = DictScope.from_kwargs()
measurement_mapping = {'a': 'a', 'b': 'b'}
channel_mapping = dict()
seq = SequencePulseTemplate(sub1, sub2, measurements=[('a', 0, 1)])
- loop = Loop()
- seq._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- global_transformation=None,
- to_single_waveform=set(),
- parent_loop=loop)
+ program_builder = LoopBuilder()
+ program_builder.override(measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, scope=scope)
+ seq._internal_build_program(program_builder=program_builder)
+ loop = program_builder.to_program()
self.assertEqual(1, loop.repetition_count)
self.assertIsNone(loop.waveform)
self.assertEqual([Loop(repetition_count=1, waveform=sub2.waveform)],
@@ -357,13 +362,10 @@ def test_internal_create_program_one_child_no_duration(self) -> None:
### test again with inverted sequence
seq = SequencePulseTemplate(sub2, sub1, measurements=[('a', 0, 1)])
- loop = Loop()
- seq._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- global_transformation=None,
- to_single_waveform=set(),
- parent_loop=loop)
+ program_builder = LoopBuilder()
+ program_builder.override(measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, scope=scope)
+ seq._internal_build_program(program_builder=program_builder)
+ loop = program_builder.to_program()
self.assertEqual(1, loop.repetition_count)
self.assertIsNone(loop.waveform)
self.assertEqual([Loop(repetition_count=1, waveform=sub2.waveform)],
@@ -374,7 +376,7 @@ def test_internal_create_program_one_child_no_duration(self) -> None:
loop.cleanup()
self.assert_measurement_windows_equal({'a': ([0], [1])}, loop.get_measurement_windows())
- def test_internal_create_program_both_children_no_duration(self) -> None:
+ def test_internal_build_program_both_children_no_duration(self) -> None:
sub1 = DummyPulseTemplate(duration=0, waveform=None, measurements=[('b', 1, 2)], defined_channels={'A'})
sub2 = DummyPulseTemplate(duration=0, waveform=None, parameter_names={'foo'}, defined_channels={'A'})
scope = DictScope.from_kwargs()
@@ -382,68 +384,44 @@ def test_internal_create_program_both_children_no_duration(self) -> None:
channel_mapping = dict()
seq = SequencePulseTemplate(sub1, sub2, measurements=[('a', 0, 1)])
- loop = Loop(measurements=None)
- seq._internal_create_program(scope=scope,
- measurement_mapping=measurement_mapping,
- channel_mapping=channel_mapping,
- global_transformation=None,
- to_single_waveform=set(),
- parent_loop=loop)
- self.assertEqual(1, loop.repetition_count)
- self.assertIsNone(loop.waveform)
- self.assertEqual([], list(loop.children))
- self.assertIsNone(loop._measurements)
+ program_builder = default_program_builder()
+ program_builder.override(measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, scope=scope)
+ seq._internal_build_program(program_builder=program_builder)
+ self.assertIsNone(program_builder.to_program())
- def test_internal_create_program_parameter_constraint_violations(self) -> None:
+ def test_build_program_parameter_constraint_violations(self) -> None:
sub1 = DummyPulseTemplate(duration=3, waveform=DummyWaveform(duration=3), measurements=[('b', 1, 2)])
sub2 = DummyPulseTemplate(duration=2, waveform=DummyWaveform(duration=2), parameter_names={'foo'})
scope = DictScope.from_kwargs(foo=7)
seq = SequencePulseTemplate(sub1, sub2, measurements=[('a', 0, 1)], parameter_constraints={'foo < 2'})
- loop = Loop()
+ program_builder = default_program_builder()
+ program_builder.override(
+ scope=scope,
+ )
with self.assertRaises(ParameterConstraintViolation):
- seq._internal_create_program(scope=scope,
- measurement_mapping={'a': 'a', 'b': 'b'},
- channel_mapping=dict(),
- global_transformation=None,
- to_single_waveform=set(),
+ seq._build_program(program_builder=program_builder)
+ self.assertIsNone(program_builder.to_program())
- parent_loop=loop)
-
- def test_internal_create_program_parameter_missing(self) -> None:
+ def test_internal_build_program_parameter_missing(self) -> None:
sub1 = DummyPulseTemplate(duration=3, waveform=DummyWaveform(duration=3), measurements=[('b', 1, 2)])
- sub2 = DummyPulseTemplate(duration='d', waveform=DummyWaveform(duration=2), parameter_names={'foo'})
+ sub2 = DummyPulseTemplate(duration=2, waveform=DummyWaveform(duration=2), parameter_names={'foo'})
seq = SequencePulseTemplate(sub1, sub2, measurements=[('a', 'bar', 1)], parameter_constraints={'foo < 2'})
- loop = Loop()
# test parameter from constraints
scope = DictScope.from_kwargs()
+ program_builder = default_program_builder()
+ program_builder.override(scope=scope, measurement_mapping={'a': 'a', 'b': 'b'})
with self.assertRaises(ParameterNotProvidedException):
- seq._internal_create_program(scope=scope,
- measurement_mapping={'a': 'a', 'b': 'b'},
- channel_mapping=dict(),
- global_transformation=None,
- to_single_waveform=set(),
- parent_loop=loop)
+ seq._build_program(program_builder=program_builder)
+ self.assertIsNone(program_builder.to_program())
# test parameter from measurements
scope = DictScope.from_mapping({'foo': 1})
+ program_builder = default_program_builder()
+ program_builder.override(scope=scope, measurement_mapping={'a': 'a', 'b': 'b'})
with self.assertRaises(ParameterNotProvidedException):
- seq._internal_create_program(scope=scope,
- measurement_mapping={'a': 'a', 'b': 'b'},
- channel_mapping=dict(),
- global_transformation=None,
- to_single_waveform=set(),
- parent_loop=loop)
-
- # test parameter from duration
- scope = DictScope.from_mapping({'foo': 1, 'bar': 0})
- with self.assertRaises(ParameterNotProvidedException):
- seq._internal_create_program(scope=scope,
- measurement_mapping={'a': 'a', 'b': 'b'},
- channel_mapping=dict(),
- global_transformation=None,
- to_single_waveform=set(),
- parent_loop=loop)
+ seq._build_program(program_builder=program_builder)
+ self.assertIsNone(program_builder.to_program())
class SequencePulseTemplateTestProperties(SequencePulseTemplateTest):
diff --git a/tests/pulses/sequencing_dummies.py b/tests/pulses/sequencing_dummies.py
index 889aea575..4e7ed09a9 100644
--- a/tests/pulses/sequencing_dummies.py
+++ b/tests/pulses/sequencing_dummies.py
@@ -1,17 +1,22 @@
"""STANDARD LIBRARY IMPORTS"""
import numbers
+import typing
from typing import Tuple, List, Dict, Optional, Set, Any, Union, Mapping
import copy
+from unittest import mock
import numpy
import unittest
+import qupulse.program.loop
+
"""LOCAL IMPORTS"""
from qupulse.parameter_scope import Scope
-from qupulse._program._loop import Loop
+from qupulse.program.loop import Loop
from qupulse.utils.types import MeasurementWindow, ChannelID, TimeType, time_from_float
from qupulse.serialization import Serializer
from qupulse._program.waveforms import Waveform
+from qupulse.program import ProgramBuilder
from qupulse.pulses.pulse_template import AtomicPulseTemplate
from qupulse.pulses.interpolation import InterpolationStrategy
from qupulse.expressions import Expression, ExpressionScalar
@@ -20,14 +25,16 @@
class MeasurementWindowTestCase(unittest.TestCase):
def assert_measurement_windows_equal(self, expected, actual) -> bool:
- self.assertEqual(expected.keys(), actual.keys())
- for k in expected:
- self.assertEqual(list(expected[k][0]), list(actual[k][0]))
- self.assertEqual(list(expected[k][1]), list(actual[k][1]))
+ def normalize_measurement_windows(mw):
+ return {name: ([bs[idx] for idx in numpy.argsort(bs)], [ls[idx] for idx in numpy.argsort(bs)])
+ for name, (bs, ls) in mw.items()}
+ expected = normalize_measurement_windows(expected)
+ actual = normalize_measurement_windows(actual)
+ self.assertEqual(expected, actual)
-class DummyWaveform(Waveform):
+class DummyWaveform(Waveform):
def __init__(self, duration: Union[float, TimeType]=0, sample_output: Union[numpy.ndarray, dict]=None, defined_channels=None) -> None:
super().__init__(duration=duration if isinstance(duration, TimeType) else TimeType.from_float(duration))
self.sample_output = sample_output
@@ -39,6 +46,12 @@ def __init__(self, duration: Union[float, TimeType]=0, sample_output: Union[nump
self.defined_channels_ = defined_channels
self.sample_calls = []
+ def __hash__(self):
+ return hash(self.compare_key)
+
+ def __eq__(self, other):
+ return isinstance(other, DummyWaveform) and self.compare_key == other.compare_key
+
@property
def compare_key(self) -> Any:
if self.sample_output is not None:
@@ -47,7 +60,8 @@ def compare_key(self) -> Any:
except AttributeError:
pass
return hash(
- tuple(sorted((channel, output.tobytes()) for channel, output in self.sample_output.items()))
+ tuple(sorted((channel, getattr(output, 'tobytes', lambda: output)())
+ for channel, output in self.sample_output.items()))
)
else:
return id(self)
@@ -142,8 +156,9 @@ def __init__(self,
final_values: Dict[ChannelID, Any]=None,
program: Optional[Loop]=None,
identifier=None,
+ metadata=None,
registry=None) -> None:
- super().__init__(identifier=identifier, measurements=measurements)
+ super().__init__(identifier=identifier, measurements=measurements, metadata=metadata)
self.requires_stop_ = requires_stop
self.requires_stop_arguments = []
@@ -176,10 +191,25 @@ def __init__(self,
if integrals is not None:
assert isinstance(integrals, Mapping)
+ self._internal_build_program = mock.MagicMock(wraps=self._internal_build_program)
+
@property
def duration(self):
return self._duration
+ def _internal_build_program(self, program_builder: ProgramBuilder):
+ measurements = self.get_measurement_windows(program_builder.build_context.scope,
+ measurement_mapping=program_builder.build_context.measurement_mapping)
+ if self._program:
+ program_builder = typing.cast(program_builder, qupulse.program.loop.LoopBuilder)
+ parent_loop = program_builder._top
+
+ parent_loop.add_measurements(measurements)
+ parent_loop.append_child(waveform=self._program.waveform, children=self._program.children)
+ elif self.waveform:
+ program_builder.measure(measurements)
+ program_builder.play_arbitrary_waveform(waveform=self.waveform)
+
@property
def parameter_names(self) -> Set[str]:
return set(self.parameter_names_)
@@ -198,15 +228,18 @@ def _internal_create_program(self, *,
channel_mapping: Dict[ChannelID, Optional[ChannelID]],
global_transformation: Optional['Transformation'],
to_single_waveform: Set[Union[str, 'PulseTemplate']],
- parent_loop: Loop) -> None:
+ program_builder: ProgramBuilder) -> None:
measurements = self.get_measurement_windows(scope, measurement_mapping)
- self.create_program_calls.append((scope, measurement_mapping, channel_mapping, parent_loop))
+ self.create_program_calls.append((scope, measurement_mapping, channel_mapping, program_builder))
if self._program:
+ program_builder = typing.cast(program_builder, qupulse.program.loop.LoopBuilder)
+ parent_loop = program_builder._top
+
parent_loop.add_measurements(measurements)
parent_loop.append_child(waveform=self._program.waveform, children=self._program.children)
elif self.waveform:
- parent_loop.add_measurements(measurements)
- parent_loop.append_child(waveform=self.waveform)
+ program_builder.measure(measurements)
+ program_builder.play_arbitrary_waveform(waveform=self.waveform)
def build_waveform(self,
parameters: Dict[str, numbers.Real],
diff --git a/tests/pulses/table_pulse_template_tests.py b/tests/pulses/table_pulse_template_tests.py
index 40414ccd5..46cf99ae8 100644
--- a/tests/pulses/table_pulse_template_tests.py
+++ b/tests/pulses/table_pulse_template_tests.py
@@ -219,15 +219,22 @@ def test_external_constraints(self):
table.build_waveform(parameters=dict(v=1., w=2, t=0.1, x=1.2, y=1, h=2),
channel_mapping={0: 0, 1: 1})
- def test_get_entries_instantiated_one_entry_float_float(self) -> None:
+ def test_get_entries_instantiated_empty(self):
table = TablePulseTemplate({0: [(0, 2)]})
+ self.assertEqual({}, table.get_entries_instantiated(dict()))
+
+ def test_get_entries_instantiated_one_entry_float_float(self) -> None:
+ table = TablePulseTemplate({0: [(1, 2)]})
instantiated_entries = table.get_entries_instantiated(dict())[0]
- self.assertEqual([(0, 2, HoldInterpolationStrategy())], instantiated_entries)
+ self.assertEqual([(0, 2, HoldInterpolationStrategy()), (1, 2, HoldInterpolationStrategy())],
+ instantiated_entries)
def test_get_entries_instantiated_one_entry_float_declaration(self) -> None:
- table = TablePulseTemplate({0: [(0, 'foo')]})
+ table = TablePulseTemplate({0: [(1, 'foo')]})
instantiated_entries = table.get_entries_instantiated({'foo': 2})[0]
- self.assertEqual([(0, 2, HoldInterpolationStrategy())], instantiated_entries)
+ self.assertEqual([(0, 2, HoldInterpolationStrategy()),
+ (1, 2, HoldInterpolationStrategy())],
+ instantiated_entries)
def test_get_entries_instantiated_two_entries_float_float_declaration_float(self) -> None:
table = TablePulseTemplate({0: [('foo', -2.)]})
diff --git a/tests/pulses/time_reversal_pulse_template_tests.py b/tests/pulses/time_reversal_pulse_template_tests.py
index 0ded84236..7e5e38ab1 100644
--- a/tests/pulses/time_reversal_pulse_template_tests.py
+++ b/tests/pulses/time_reversal_pulse_template_tests.py
@@ -1,12 +1,17 @@
import unittest
+import numpy as np
+
+from qupulse.pulses import ConstantPT, FunctionPT
+from qupulse.plotting import render
from qupulse.pulses.time_reversal_pulse_template import TimeReversalPulseTemplate
from qupulse.utils.types import TimeType
from qupulse.expressions import ExpressionScalar
-
+from qupulse.program.loop import LoopBuilder
+from qupulse.program.linspace import LinSpaceBuilder, LinSpaceVM, to_increment_commands
from tests.pulses.sequencing_dummies import DummyPulseTemplate
from tests.serialization_tests import SerializableTests
-
+from tests.program.linspace_tests import assert_vm_output_almost_equal
class TimeReversalPulseTemplateTests(unittest.TestCase):
def test_simple_properties(self):
@@ -25,6 +30,59 @@ def test_simple_properties(self):
self.assertEqual(reversed_pt.identifier, 'reverse')
+ def test_time_reversal_loop(self):
+ inner = ConstantPT(4, {'a': 3}) @ FunctionPT('sin(t)', 5, channel='a')
+ manual_reverse = FunctionPT('sin(5 - t)', 5, channel='a') @ ConstantPT(4, {'a': 3})
+ time_reversed = TimeReversalPulseTemplate(inner)
+
+ program = time_reversed.create_program(program_builder=LoopBuilder())
+ manual_program = manual_reverse.create_program(program_builder=LoopBuilder())
+
+ t, data, _ = render(program, 9 / 10)
+ _, manual_data, _ = render(manual_program, 9 / 10)
+
+ np.testing.assert_allclose(data['a'], manual_data['a'])
+
+ def test_time_reversal_linspace(self):
+ constant_pt = ConstantPT(4, {'a': '3.0 + x * 1.0 + y * -0.3'})
+ function_pt = FunctionPT('sin(t)', 5, channel='a')
+ reversed_function_pt = function_pt.with_time_reversal()
+
+ inner = (constant_pt @ function_pt).with_iteration('x', 6)
+ inner_manual = (reversed_function_pt @ constant_pt).with_iteration('x', (5, -1, -1))
+
+ outer = inner.with_time_reversal().with_iteration('y', 8)
+ outer_man = inner_manual.with_iteration('y', 8)
+
+ self.assertEqual(outer.duration, outer_man.duration)
+
+ program = outer.create_program(program_builder=LinSpaceBuilder(channels=('a',)))
+ manual_program = outer_man.create_program(program_builder=LinSpaceBuilder(channels=('a',)))
+
+ commands = to_increment_commands(program)
+ manual_commands = to_increment_commands(manual_program)
+ self.assertEqual(commands, manual_commands)
+
+ manual_vm = LinSpaceVM(1)
+ manual_vm.set_commands(manual_commands)
+ manual_vm.run()
+
+ vm = LinSpaceVM(1)
+ vm.set_commands(commands)
+ vm.run()
+
+ assert_vm_output_almost_equal(self, manual_vm.history, vm.history)
+
+ def test_initial_final_values(self):
+
+ pt = FunctionPT('-1+t/t_gate*3','t_gate','a')
+ r_pt = pt.with_time_reversal()
+
+ self.assertEqual(r_pt.initial_values, pt.final_values)
+ self.assertEqual(r_pt.final_values, pt.initial_values)
+ self.assertAlmostEqual(float(r_pt.initial_values['a']), 2, places=8)
+ self.assertAlmostEqual(float(r_pt.final_values['a']), -1, places=8)
+
class TimeReversalPulseTemplateSerializationTests(unittest.TestCase, SerializableTests):
@property
diff --git a/tests/qctoolkit_alias_tests.py b/tests/qctoolkit_alias_tests.py
deleted file mode 100644
index 5c7a5d834..000000000
--- a/tests/qctoolkit_alias_tests.py
+++ /dev/null
@@ -1,16 +0,0 @@
-import unittest
-
-
-class QctoolkitAliasTest(unittest.TestCase):
- def test_alias(self):
- import qctoolkit.pulses
- import qupulse.pulses
-
- self.assertIs(qctoolkit.pulses, qupulse.pulses)
- self.assertIs(qctoolkit.pulses.TablePT, qupulse.pulses.TablePT)
-
- def test_class_identity(self):
- from qupulse._program._loop import Loop as Loop_qu
- from qctoolkit._program._loop import Loop as Loop_qc
-
- self.assertIs(Loop_qc, Loop_qu)
diff --git a/tests/serialization_tests.py b/tests/serialization_tests.py
index f230bd6da..e980c096c 100644
--- a/tests/serialization_tests.py
+++ b/tests/serialization_tests.py
@@ -25,9 +25,16 @@
class DummySerializable(Serializable):
- def __init__(self, identifier: Optional[str]=None, registry: PulseRegistryType=None, **kwargs) -> None:
+ def __init__(self,
+ identifier: Optional[str]=None,
+ registry: PulseRegistryType=None,
+ **kwargs) -> None:
super().__init__(identifier)
for name in kwargs:
+ if name == "metadata":
+ # this was the easiest way to adjust this test
+ # metadata is not a Serializable but a PulseTemplate attribute so it is a hacky solution
+ continue
setattr(self, name, kwargs[name])
self._register(registry=registry)
@@ -79,13 +86,15 @@ def assert_equal_instance(self, lhs, rhs):
def assert_equal_instance_except_id(self, lhs, rhs):
pass
- def make_instance(self, identifier=None, registry=None):
- return self.class_to_test(identifier=identifier, registry=registry, **self.make_kwargs())
+ def make_instance(self, identifier=None, registry=None, metadata=None):
+ return self.class_to_test(identifier=identifier, registry=registry, metadata=metadata, **self.make_kwargs())
- def make_serialization_data(self, identifier=None):
+ def make_serialization_data(self, identifier=None, metadata=None):
data = {Serializable.type_identifier_name: self.class_to_test.get_type_identifier(), **self.make_kwargs()}
if identifier:
data[Serializable.identifier_name] = identifier
+ if metadata:
+ data["metadata"] = metadata
return data
def test_identifier(self) -> None:
@@ -736,7 +745,7 @@ def my_callable():
self.assertIs(finder['qctoolkit.asd'], my_callable)
finder.qctoolkit_alias = False
- with self.assertRaises(KeyError):
+ with self.assertRaises(ModuleNotFoundError):
finder['qctoolkit.asd']
@@ -1051,9 +1060,12 @@ def test_deserialize_storage_is_not_default_registry_id_occupied(self) -> None:
del pulse_storage
pulse_storage = PulseStorage(backend)
- with self.assertRaisesRegex(RuntimeError, "Pulse with name already exists"):
+ with self.assertRaises(ValueError) as cm:
pulse_storage['peter']
+ # this is shitty
+ self.assertIsInstance(cm.exception.__cause__, RuntimeError)
+
def test_deserialize_twice_same_object_storage_is_default_registry(self) -> None:
backend = DummyStorageBackend()
@@ -1524,8 +1536,8 @@ def test_convert_stored_pulse_in_storage_dest_not_empty_id_overlap(self) -> None
with self.assertRaises(ValueError):
convert_stored_pulse_in_storage('hugos_parent', source_backend, destination_backend)
- self.assertEquals('already_existing_data', destination_backend['hugo'])
- self.assertEquals(1, len(destination_backend.stored_items))
+ self.assertEqual('already_existing_data', destination_backend['hugo'])
+ self.assertEqual(1, len(destination_backend.stored_items))
def test_convert_stored_pulse_in_storage_dest_not_empty_no_id_overlap(self) -> None:
with warnings.catch_warnings():
@@ -1545,7 +1557,7 @@ def test_convert_stored_pulse_in_storage_dest_not_empty_no_id_overlap(self) -> N
destination_backend.put('ilse', 'already_existing_data')
convert_stored_pulse_in_storage('hugos_parent', source_backend, destination_backend)
- self.assertEquals('already_existing_data', destination_backend['ilse'])
+ self.assertEqual('already_existing_data', destination_backend['ilse'])
pulse_storage = PulseStorage(destination_backend)
deserialized = pulse_storage['hugos_parent']
self.assertEqual(serializable, deserialized)
@@ -1604,8 +1616,8 @@ def test_convert_stored_pulses_dest_not_empty_id_overlap(self) -> None:
with self.assertRaises(ValueError):
convert_pulses_in_storage(source_backend, destination_backend)
- self.assertEquals('already_existing_data', destination_backend['hugo'])
- self.assertEquals(1, len(destination_backend.stored_items))
+ self.assertEqual('already_existing_data', destination_backend['hugo'])
+ self.assertEqual(1, len(destination_backend.stored_items))
def test_convert_stored_pulses_dest_not_empty_no_id_overlap(self) -> None:
with warnings.catch_warnings():
diff --git a/tests/utils/numeric_tests.py b/tests/utils/numeric_tests.py
index b632f058b..38410c682 100644
--- a/tests/utils/numeric_tests.py
+++ b/tests/utils/numeric_tests.py
@@ -5,7 +5,7 @@
from collections import deque
from itertools import islice
-from qupulse.utils.numeric import approximate_rational, approximate_double, smallest_factor_ge
+from qupulse.utils.numeric import approximate_rational, approximate_double, smallest_factor_ge, lcm
def stern_brocot_sequence() -> Iterator[int]:
@@ -120,3 +120,14 @@ def test_smallest_factor_ge(self):
self.assertEqual(smallest_factor_ge(45, 4), 5, brute_force)
self.assertEqual(smallest_factor_ge(45, 5), 5, brute_force)
self.assertEqual(smallest_factor_ge(36, 8), 9, brute_force)
+
+
+class LeastCommonMultipleTests(unittest.TestCase):
+ def test_few_args(self):
+ self.assertEqual(1, lcm())
+ self.assertEqual(5, lcm(5))
+
+ def test_multi_args(self):
+ self.assertEqual(15, lcm(3, 5))
+ self.assertEqual(0, lcm(3, 0))
+ self.assertEqual(20, lcm(2, 5, 4, 10))
diff --git a/tests/utils/performance_tests.py b/tests/utils/performance_tests.py
index d158dce5c..bc31960ae 100644
--- a/tests/utils/performance_tests.py
+++ b/tests/utils/performance_tests.py
@@ -1,8 +1,12 @@
import unittest
+import warnings
import numpy as np
-from qupulse.utils.performance import _time_windows_to_samples_numba, _time_windows_to_samples_numpy
+from qupulse.utils.performance import (
+ _time_windows_to_samples_numba, _time_windows_to_samples_numpy,
+ _average_windows_numba, _average_windows_numpy, average_windows,
+ shrink_overlapping_windows, WindowOverlapWarning)
class TimeWindowsToSamplesTest(unittest.TestCase):
@@ -28,3 +32,79 @@ def test_unsorted(self):
self.assert_implementations_equal(begins, lengths, sr)
+class WindowAverageTest(unittest.TestCase):
+ @staticmethod
+ def assert_implementations_equal(time, values, begins, ends):
+ numpy_result = _average_windows_numpy(time, values, begins, ends)
+ numba_result = _average_windows_numba(time, values, begins, ends)
+ np.testing.assert_allclose(numpy_result, numba_result)
+
+ def setUp(self):
+ self.begins = np.array([1., 2., 3.] + [4.] + [6., 7., 8., 9., 10.])
+ self.ends = self.begins + np.array([1., 1., 1.] + [3.] + [2., 2., 2., 2., 2.])
+ self.time = np.arange(10).astype(float)
+ self.values = np.asarray([
+ np.sin(self.time),
+ np.cos(self.time),
+ ]).T
+
+ def test_dispatch(self):
+ _ = average_windows(self.time, self.values, self.begins, self.ends)
+ _ = average_windows(self.time, self.values[..., 0], self.begins, self.ends)
+
+ def test_single_channel(self):
+ self.assert_implementations_equal(self.time, self.values[..., 0], self.begins, self.ends)
+ self.assert_implementations_equal(self.time, self.values[..., :1], self.begins, self.ends)
+
+ def test_dual_channel(self):
+ self.assert_implementations_equal(self.time, self.values, self.begins, self.ends)
+
+
+class TestOverlappingWindowReduction(unittest.TestCase):
+ def setUp(self):
+ self.shrank = np.array([1, 4, 8], dtype=np.uint64), np.array([3, 4, 4], dtype=np.uint64)
+ self.to_shrink = np.array([1, 4, 7], dtype=np.uint64), np.array([3, 4, 5], dtype=np.uint64)
+
+ def assert_noop(self, shrink_fn):
+ begins = np.array([1, 3, 5], dtype=np.uint64)
+ lengths = np.array([2, 1, 6], dtype=np.uint64)
+ result = shrink_fn(begins, lengths)
+ np.testing.assert_equal((begins, lengths), result)
+
+ begins = (np.arange(100) * 176.5).astype(dtype=np.uint64)
+ lengths = (np.ones(100) * 10 * np.pi).astype(dtype=np.uint64)
+ result = shrink_fn(begins, lengths)
+ np.testing.assert_equal((begins, lengths), result)
+
+ begins = np.arange(15, dtype=np.uint64)*16
+ lengths = 1+np.arange(15, dtype=np.uint64)
+ result = shrink_fn(begins, lengths)
+ np.testing.assert_equal((begins, lengths), result)
+
+ def assert_shrinks(self, shrink_fn):
+ with warnings.catch_warnings():
+ warnings.simplefilter("always", WindowOverlapWarning)
+ with self.assertWarns(WindowOverlapWarning):
+ shrank = shrink_fn(*self.to_shrink)
+ np.testing.assert_equal(self.shrank, shrank)
+
+ def assert_empty_window_error(self, shrink_fn):
+ invalid = np.array([1, 2], dtype=np.uint64), np.array([5, 1], dtype=np.uint64)
+ with self.assertRaisesRegex(ValueError, "Overlap is bigger than measurement window"):
+ shrink_fn(*invalid)
+
+ def test_shrink_overlapping_windows_numba(self):
+ def shrink_fn(begins, lengths):
+ return shrink_overlapping_windows(begins, lengths, use_numba=True)
+
+ self.assert_noop(shrink_fn)
+ self.assert_shrinks(shrink_fn)
+ self.assert_empty_window_error(shrink_fn)
+
+ def test_shrink_overlapping_windows_numpy(self):
+ def shrink_fn(begins, lengths):
+ return shrink_overlapping_windows(begins, lengths, use_numba=False)
+
+ self.assert_noop(shrink_fn)
+ self.assert_shrinks(shrink_fn)
+ self.assert_empty_window_error(shrink_fn)
diff --git a/tests/utils/sympy_tests.py b/tests/utils/sympy_tests.py
index 871ac2443..38f7e9e10 100644
--- a/tests/utils/sympy_tests.py
+++ b/tests/utils/sympy_tests.py
@@ -14,8 +14,13 @@
import sympy
import numpy as np
+try:
+ import scipy
+except ImportError:
+ scipy = None
+
from sympy.abc import a, b, c, d, e, f, k, l, m, n, i, j
-from sympy import sin, Sum, IndexedBase, Rational
+from sympy import sin, Sum, IndexedBase, Rational, Integral
a_ = IndexedBase(a)
b_ = IndexedBase(b)
@@ -120,6 +125,9 @@
# TODO: this fails
# (np.array([a, Rational(1, 3)]), {'a': 2}, np.array([2, TimeType.from_fraction(1, 3)]))
]
+eval_integral = [
+ (Integral(sin(b * a ** 2 + c * a) / a, (a, 0, c))/b, {'b': 5, 'c': 100.}, 0.26302083739430604)
+]
class TestCase(unittest.TestCase):
@@ -305,6 +313,24 @@ def test_eval_exact_rational(self):
except ValueError:
np.testing.assert_equal(result, expected)
+ def test_integral(self):
+ if type(self) is CompiledEvaluationTest:
+ raise unittest.SkipTest("Integrals are not representable in pure repr lambdas.")
+
+ if scipy is None:
+ # printer based evaluate requires scipy to print integrals
+ for expr, parameters, _ in eval_integral:
+ with self.assertRaises(NotImplementedError):
+ self.evaluate(expr, parameters)
+ return
+
+ for expr, parameters, expected in eval_integral:
+ result = self.evaluate(expr, parameters)
+ try:
+ self.assertEqual(result, expected)
+ except ValueError:
+ np.testing.assert_equal(result, expected)
+
class LamdifiedEvaluationTest(EvaluationTestsBase, unittest.TestCase):
def evaluate(self, expression: Union[sympy.Expr, np.ndarray], parameters):
@@ -345,7 +371,11 @@ def evaluate(self, expression: Union[sympy.Expr, np.ndarray], parameters):
if isinstance(expression, np.ndarray):
return self.evaluate(sympy.Array(expression), parameters)
- result, _ = evaluate_compiled(expression, parameters, compiled=None)
+ try:
+ result, _ = evaluate_compiled(expression, parameters, compiled=None)
+ except Exception as err:
+ raise AssertionError(f"Compiled evaluation of {expression!r} with {parameters!r} failed: {err!r}",
+ expression, parameters) from err
if isinstance(result, (list, tuple)):
return np.array(result)
diff --git a/tests/utils/time_type_tests.py b/tests/utils/time_type_tests.py
index 93e118325..b069cacf4 100644
--- a/tests/utils/time_type_tests.py
+++ b/tests/utils/time_type_tests.py
@@ -1,266 +1,200 @@
-import sys
import unittest
-import builtins
-import contextlib
-import importlib
import fractions
import random
-from unittest import mock
-
-try:
- import gmpy2
-except ImportError:
- gmpy2 = None
import numpy as np
import sympy
+import gmpy2
-import qupulse.utils.types as qutypes
-
-
-@contextlib.contextmanager
-def mock_missing_module(module_name: str):
- exit_stack = contextlib.ExitStack()
-
- if module_name in sys.modules:
- # temporarily remove gmpy2 from the imported modules
+from qupulse.utils.types import TimeType, time_from_float
- temp_modules = sys.modules.copy()
- del temp_modules[module_name]
- exit_stack.enter_context(mock.patch.dict(sys.modules, temp_modules))
- original_import = builtins.__import__
+def assert_from_fraction_works(test: unittest.TestCase, time_type):
+ t = time_type.from_fraction(43, 12)
+ test.assertIsInstance(t, time_type)
+ test.assertEqual(t, fractions.Fraction(43, 12))
- def mock_import(name, *args, **kwargs):
- if name == module_name:
- raise ImportError(name)
- else:
- return original_import(name, *args, **kwargs)
- exit_stack.enter_context(mock.patch('builtins.__import__', mock_import))
-
- with exit_stack:
- yield
+def assert_from_float_exact_works(test: unittest.TestCase, time_type):
+ test.assertEqual(time_type.from_float(123 / 931, 0),
+ fractions.Fraction(123 / 931))
+
+
+def assert_fraction_time_from_float_with_precision_works(test: unittest.TestCase, time_type):
+ test.assertEqual(time_type.from_float(1000000 / 1000001, 1e-5),
+ fractions.Fraction(1))
+ test.assertEqual(time_type.from_float(2.50000000000008, absolute_error=1e-10),
+ time_type.from_fraction(5, 2))
+ test.assertEqual(time_type.from_float(9926.666666667, absolute_error=1e-9),
+ time_type.from_fraction(29780, 3))
+
+
+def assert_from_float_no_extra_args_works(test: unittest.TestCase, time_type):
+ # test that float(from_float(x)) == x
+ base_floats = [4/5, 1, 1000, 0, np.pi, 1.23456789**99, 1e-100, 2**53]
+ n_steps = 10**2
+
+ def float_generator():
+ for f in base_floats:
+ for _ in range(n_steps):
+ yield f
+ f = np.nextafter(f, float('inf'))
+
+ for f in base_floats:
+ for _ in range(n_steps):
+ yield f
+ f = np.nextafter(f, float('-inf'))
+
+ for x in float_generator():
+ t = time_type.from_float(x)
+ t2x = float(t)
+ test.assertEqual(x, t2x)
+ test.assertGreater(t, np.nextafter(x, float('-inf')))
+ test.assertLess(t, np.nextafter(x, float('inf')))
+
+
+def assert_try_from_any_works(test: unittest.TestCase, time_type):
+ try_from_any = time_type._try_from_any
+
+ # these duck types are here because isinstance(, numbers.) is version dependent
+ class DuckTypeWrapper:
+ def __init__(self, value):
+ self.value = value
+
+ def __repr__(self):
+ return f'{type(self)}({self.value})'
+
+ class DuckInt(DuckTypeWrapper):
+ def __int__(self):
+ return int(self.value)
+
+ class DuckFloat(DuckTypeWrapper):
+ def __float__(self):
+ return float(self.value)
+
+ class DuckIntFloat(DuckFloat):
+ def __int__(self):
+ return int(self.value)
+
+ class DuckRational:
+ def __init__(self, numerator, denominator):
+ self.numerator = numerator
+ self.denominator = denominator
+
+ def __repr__(self):
+ return f'{type(self)}({self.numerator}, {self.denominator})'
+
+ for_array_tests = []
+
+ signed_int_types = [int, sympy.Integer, np.int8, np.int16, np.int32, np.int64, DuckInt, DuckIntFloat, gmpy2.mpz]
+
+ for s_t in signed_int_types:
+ for val in (1, 17, -17):
+ any_val = s_t(val)
+ expected_val = time_type.from_fraction(int(val), 1)
+ test.assertEqual(expected_val, try_from_any(any_val))
+ for_array_tests.append((expected_val, any_val))
+
+ unsigned_int_types = [np.uint8, np.uint16, np.uint32, np.uint]
+ for u_t in unsigned_int_types:
+ for val in (1, 17):
+ any_val = u_t(val)
+ expected_val = time_type.from_fraction(int(val), 1)
+ test.assertEqual(expected_val, try_from_any(any_val))
+ for_array_tests.append((expected_val, any_val))
+
+ rational_types = [fractions.Fraction, sympy.Rational, time_type.from_fraction, DuckRational]
+ if gmpy2:
+ rational_types.append(gmpy2.mpq)
+ for r_t in rational_types:
+ for num, den in ((1, 3), (-3, 8), (17, 5)):
+ any_val = r_t(num, den)
+ expected_val = time_type.from_fraction(num, den)
+ test.assertEqual(expected_val, try_from_any(any_val))
+ for_array_tests.append((expected_val, any_val))
+
+ float_types = [float, sympy.Float, DuckFloat, DuckIntFloat]
+ if gmpy2:
+ float_types.append(gmpy2.mpfr)
+ for f_t in float_types:
+ for val in (3.4, -3., 1.):
+ any_val = f_t(val)
+ expected_val = time_type.from_float(val)
+ test.assertEqual(expected_val, try_from_any(any_val))
+ for_array_tests.append((expected_val, any_val))
+
+ arr = np.array(for_array_tests, dtype='O')
+ any_arr = arr[:, 1]
+ expected_arr = arr[:, 0]
+ np.testing.assert_equal(expected_arr, try_from_any(any_arr))
+
+
+def assert_comparisons_work(test: unittest.TestCase, time_type):
+ tt = time_type.from_float(1.1)
+
+ test.assertLess(tt, 4)
+ test.assertLess(tt, 4.)
+ test.assertLess(tt, time_type.from_float(4.))
+ test.assertLess(tt, float('inf'))
+
+ test.assertLessEqual(tt, 4)
+ test.assertLessEqual(tt, 4.)
+ test.assertLessEqual(tt, time_type.from_float(4.))
+ test.assertLessEqual(tt, float('inf'))
+
+ test.assertGreater(tt, 1)
+ test.assertGreater(tt, 1.)
+ test.assertGreater(tt, time_type.from_float(1.))
+ test.assertGreater(tt, float('-inf'))
+
+ test.assertGreaterEqual(tt, 1)
+ test.assertGreaterEqual(tt, 1.)
+ test.assertGreaterEqual(tt, time_type.from_float(1.))
+ test.assertGreaterEqual(tt, float('-inf'))
+
+ test.assertFalse(tt == float('nan'))
+ test.assertFalse(tt <= float('nan'))
+ test.assertFalse(tt >= float('nan'))
+ test.assertFalse(tt < float('nan'))
+ test.assertFalse(tt > float('nan'))
class TestTimeType(unittest.TestCase):
- """The fallback test is here for convenience while developing. The fallback is also tested by the CI explicitly"""
-
- _fallback_qutypes = None
-
- @property
- def fallback_qutypes(self):
- if not self._fallback_qutypes:
- if gmpy2:
- with mock_missing_module('gmpy2'):
- self._fallback_qutypes = importlib.reload(qutypes)
-
- else:
- self._fallback_qutypes = qutypes
- return self._fallback_qutypes
+ """Tests the TimeType class. The layout of this test is in this way for historic reasons, i.e. to allow testing
+ different internal representations for the time type. Right now only gmpy.mpq is implemented and tested."""
def test_non_finite_float(self):
with self.assertRaisesRegex(ValueError, 'Cannot represent'):
- qutypes.TimeType.from_float(float('inf'))
+ TimeType.from_float(float('inf'))
with self.assertRaisesRegex(ValueError, 'Cannot represent'):
- qutypes.TimeType.from_float(float('-inf'))
+ TimeType.from_float(float('-inf'))
with self.assertRaisesRegex(ValueError, 'Cannot represent'):
- qutypes.TimeType.from_float(float('nan'))
-
- def test_fraction_fallback(self):
- self.assertIs(fractions.Fraction, self.fallback_qutypes.TimeType._InternalType)
-
- def assert_from_fraction_works(self, time_type):
- t = time_type.from_fraction(43, 12)
- self.assertIsInstance(t, time_type)
- self.assertEqual(t, fractions.Fraction(43, 12))
+ TimeType.from_float(float('nan'))
def test_fraction_time_from_fraction(self):
- self.assert_from_fraction_works(qutypes.TimeType)
-
- @unittest.skipIf(gmpy2 is None, "fallback already tested")
- def test_fraction_time_from_fraction_fallback(self):
- self.assert_from_fraction_works(self.fallback_qutypes.TimeType)
-
- def assert_from_float_exact_works(self, time_type):
- self.assertEqual(time_type.from_float(123 / 931, 0),
- fractions.Fraction(123 / 931))
+ assert_from_fraction_works(self, TimeType)
def test_fraction_time_from_float_exact(self):
- self.assert_from_float_exact_works(qutypes.TimeType)
-
- @unittest.skipIf(gmpy2 is None, "fallback already tested")
- def test_fraction_time_from_float_exact_fallback(self):
- self.assert_from_float_exact_works(self.fallback_qutypes.TimeType)
-
- def assert_fraction_time_from_float_with_precision_works(self, time_type):
- self.assertEqual(time_type.from_float(1000000 / 1000001, 1e-5),
- fractions.Fraction(1))
- self.assertEqual(time_type.from_float(2.50000000000008, absolute_error=1e-10),
- time_type.from_fraction(5, 2))
- self.assertEqual(time_type.from_float(9926.666666667, absolute_error=1e-9),
- time_type.from_fraction(29780, 3))
+ assert_from_float_exact_works(self, TimeType)
def test_fraction_time_from_float_with_precision(self):
- self.assert_fraction_time_from_float_with_precision_works(qutypes.TimeType)
-
- @unittest.skipIf(gmpy2 is None, "fallback already tested")
- def test_fraction_time_from_float_with_precision_fallback(self):
- self.assert_fraction_time_from_float_with_precision_works(self.fallback_qutypes.TimeType)
-
- def assert_from_float_no_extra_args_works(self, time_type):
- # test that float(from_float(x)) == x
- base_floats = [4/5, 1, 1000, 0, np.pi, 1.23456789**99, 1e-100, 2**53]
- n_steps = 10**2
-
- def float_generator():
- for f in base_floats:
- for _ in range(n_steps):
- yield f
- f = np.nextafter(f, float('inf'))
-
- for f in base_floats:
- for _ in range(n_steps):
- yield f
- f = np.nextafter(f, float('-inf'))
-
- for x in float_generator():
- t = time_type.from_float(x)
- t2x = float(t)
- self.assertEqual(x, t2x)
- self.assertGreater(t, np.nextafter(x, float('-inf')))
- self.assertLess(t, np.nextafter(x, float('inf')))
+ assert_fraction_time_from_float_with_precision_works(self, TimeType)
def test_from_float_no_extra_args(self):
- self.assert_from_float_exact_works(qutypes.TimeType)
-
- @unittest.skipIf(gmpy2 is None, "fallback already tested")
- def test_from_float_no_extra_args_fallback(self):
- self.assert_from_float_exact_works(self.fallback_qutypes.TimeType)
+ assert_from_float_exact_works(self, TimeType)
def test_from_float_exceptions(self):
with self.assertRaisesRegex(ValueError, '> 0'):
- qutypes.time_from_float(.8, -1)
+ time_from_float(.8, -1)
with self.assertRaisesRegex(ValueError, '<= 1'):
- qutypes.time_from_float(.8, 2)
-
- def assert_try_from_any_works(self, time_type):
- try_from_any = time_type._try_from_any
-
- # these duck types are here because isinstance(, numbers.) is version dependent
- class DuckTypeWrapper:
- def __init__(self, value):
- self.value = value
-
- def __repr__(self):
- return f'{type(self)}({self.value})'
-
- class DuckInt(DuckTypeWrapper):
- def __int__(self):
- return int(self.value)
-
- class DuckFloat(DuckTypeWrapper):
- def __float__(self):
- return float(self.value)
-
- class DuckIntFloat(DuckFloat):
- def __int__(self):
- return int(self.value)
-
- class DuckRational:
- def __init__(self, numerator, denominator):
- self.numerator = numerator
- self.denominator = denominator
-
- def __repr__(self):
- return f'{type(self)}({self.numerator}, {self.denominator})'
-
- for_array_tests = []
-
- signed_int_types = [int, sympy.Integer, np.int8, np.int16, np.int32, np.int64, DuckInt, DuckIntFloat]
- if gmpy2:
- signed_int_types.append(gmpy2.mpz)
-
- for s_t in signed_int_types:
- for val in (1, 17, -17):
- any_val = s_t(val)
- expected_val = time_type.from_fraction(int(val), 1)
- self.assertEqual(expected_val, try_from_any(any_val))
- for_array_tests.append((expected_val, any_val))
-
- unsigned_int_types = [np.uint8, np.uint16, np.uint32, np.uint]
- for u_t in unsigned_int_types:
- for val in (1, 17):
- any_val = u_t(val)
- expected_val = time_type.from_fraction(int(val), 1)
- self.assertEqual(expected_val, try_from_any(any_val))
- for_array_tests.append((expected_val, any_val))
-
- rational_types = [fractions.Fraction, sympy.Rational, time_type.from_fraction, DuckRational]
- if gmpy2:
- rational_types.append(gmpy2.mpq)
- for r_t in rational_types:
- for num, den in ((1, 3), (-3, 8), (17, 5)):
- any_val = r_t(num, den)
- expected_val = time_type.from_fraction(num, den)
- self.assertEqual(expected_val, try_from_any(any_val))
- for_array_tests.append((expected_val, any_val))
-
- float_types = [float, sympy.Float, DuckFloat, DuckIntFloat]
- if gmpy2:
- float_types.append(gmpy2.mpfr)
- for f_t in float_types:
- for val in (3.4, -3., 1.):
- any_val = f_t(val)
- expected_val = time_type.from_float(val)
- self.assertEqual(expected_val, try_from_any(any_val))
- for_array_tests.append((expected_val, any_val))
-
- arr = np.array(for_array_tests, dtype='O')
- any_arr = arr[:, 1]
- expected_arr = arr[:, 0]
- np.testing.assert_equal(expected_arr, try_from_any(any_arr))
+ time_from_float(.8, 2)
def test_try_from_any(self):
- self.assert_try_from_any_works(qutypes.TimeType)
- self.assert_try_from_any_works(self.fallback_qutypes.TimeType)
-
- def assert_comparisons_work(self, time_type):
- tt = time_type.from_float(1.1)
-
- self.assertLess(tt, 4)
- self.assertLess(tt, 4.)
- self.assertLess(tt, time_type.from_float(4.))
- self.assertLess(tt, float('inf'))
-
- self.assertLessEqual(tt, 4)
- self.assertLessEqual(tt, 4.)
- self.assertLessEqual(tt, time_type.from_float(4.))
- self.assertLessEqual(tt, float('inf'))
-
- self.assertGreater(tt, 1)
- self.assertGreater(tt, 1.)
- self.assertGreater(tt, time_type.from_float(1.))
- self.assertGreater(tt, float('-inf'))
-
- self.assertGreaterEqual(tt, 1)
- self.assertGreaterEqual(tt, 1.)
- self.assertGreaterEqual(tt, time_type.from_float(1.))
- self.assertGreaterEqual(tt, float('-inf'))
-
- self.assertFalse(tt == float('nan'))
- self.assertFalse(tt <= float('nan'))
- self.assertFalse(tt >= float('nan'))
- self.assertFalse(tt < float('nan'))
- self.assertFalse(tt > float('nan'))
+ assert_try_from_any_works(self, TimeType)
def test_comparisons_work(self):
- self.assert_comparisons_work(qutypes.TimeType)
-
- @unittest.skipIf(gmpy2 is None, "fallback already tested")
- def test_comparisons_work_fallback(self):
- self.assert_comparisons_work(self.fallback_qutypes.TimeType)
+ assert_comparisons_work(self, TimeType)
def get_some_floats(seed=42, n=1000):
@@ -269,7 +203,7 @@ def get_some_floats(seed=42, n=1000):
def get_from_float(fs):
- return [qutypes.time_from_float(f) for f in fs]
+ return [time_from_float(f) for f in fs]
def do_additions(xs, ys):
diff --git a/tests/utils/types_tests.py b/tests/utils/types_tests.py
index e2271d2fe..5a2a53ef1 100644
--- a/tests/utils/types_tests.py
+++ b/tests/utils/types_tests.py
@@ -1,10 +1,8 @@
import unittest
-import inspect
import numpy as np
-from qupulse.utils.types import (HashableNumpyArray, SequenceProxy, _FrozenDictByWrapping,
- _FrozenDictByInheritance)
+from qupulse.utils.types import (HashableNumpyArray, SequenceProxy,)
class HashableNumpyArrayTest(unittest.TestCase):
@@ -36,101 +34,3 @@ def test_sequence_proxy(self):
with self.assertRaises(TypeError):
p[1] = 7
-
-
-class FrozenDictTests(unittest.TestCase):
- FrozenDictType = _FrozenDictByWrapping
-
- """This class can test general non mutable mappings"""
- def setUp(self) -> None:
- self.d = {'a': 1, 'b': 2}
- self.f = self.FrozenDictType(self.d)
- self.prev_state = dict(self.f)
-
- def tearDown(self) -> None:
- self.assertEqual(self.prev_state, dict(self.f))
-
- def test_init(self):
- d = {'a': 1, 'b': 2}
-
- f1 = self.FrozenDictType(d)
- f2 = self.FrozenDictType(**d)
- f3 = self.FrozenDictType(d.items())
-
- self.assertEqual(d, f1)
- self.assertEqual(d, f2)
- self.assertEqual(d, f3)
-
- self.assertEqual(d.keys(), f1.keys())
- self.assertEqual(d.keys(), f2.keys())
- self.assertEqual(d.keys(), f3.keys())
-
- self.assertEqual(set(d.items()), set(f1.items()))
- self.assertEqual(set(d.items()), set(f2.items()))
- self.assertEqual(set(d.items()), set(f3.items()))
-
- def test_mapping(self):
- d = {'a': 1, 'b': 2}
- f = self.FrozenDictType(d)
-
- self.assertEqual(len(d), len(f))
- self.assertIn('a', f)
- self.assertIn('b', f)
- self.assertNotIn('c', f)
-
- self.assertEqual(1, f['a'])
- self.assertEqual(2, f['b'])
-
- with self.assertRaisesRegex(KeyError, 'c'):
- _ = f['c']
-
- with self.assertRaises(TypeError):
- f['a'] = 9
-
- with self.assertRaises(TypeError):
- del f['a']
-
- def test_copy(self):
- d = {'a': 1, 'b': 2}
- f = self.FrozenDictType(d)
- self.assertIs(f, f.copy())
-
- def test_eq_and_hash(self):
- d = {'a': 1, 'b': 2}
-
- f1 = self.FrozenDictType(d)
- f2 = self.FrozenDictType({'a': 1, 'b': 2})
- f3 = self.FrozenDictType({'a': 1, 'c': 3})
-
- self.assertEqual(f1, f2)
- self.assertEqual(hash(f1), hash(f2))
-
- self.assertNotEqual(f1, f3)
-
-
-class FrozenDictByInheritanceTests(FrozenDictTests):
- FrozenDictType = _FrozenDictByInheritance
-
- def test_update(self):
- with self.assertRaisesRegex(TypeError, 'immutable'):
- self.f.update(d=5)
-
- def test_setdefault(self):
- with self.assertRaisesRegex(TypeError, 'immutable'):
- self.f.setdefault('c', 3)
- with self.assertRaisesRegex(TypeError, 'immutable'):
- self.f.setdefault('a', 2)
-
- def test_clear(self):
- with self.assertRaisesRegex(TypeError, 'immutable'):
- self.f.clear()
-
- def test_pop(self):
- with self.assertRaisesRegex(TypeError, 'immutable'):
- self.f.pop()
- with self.assertRaisesRegex(TypeError, 'immutable'):
- self.f.pop('a')
-
- def test_popitem(self):
- with self.assertRaisesRegex(TypeError, 'immutable'):
- self.f.popitem()
diff --git a/tests/utils/utils_tests.py b/tests/utils/utils_tests.py
index 6ec75092c..a6d880e8a 100644
--- a/tests/utils/utils_tests.py
+++ b/tests/utils/utils_tests.py
@@ -2,7 +2,7 @@
from unittest import mock
from collections import OrderedDict
-from qupulse.utils import checked_int_cast, replace_multiple, _fallback_pairwise
+from qupulse.utils import checked_int_cast, replace_multiple, _fallback_pairwise, to_next_multiple
class PairWiseTest(unittest.TestCase):
@@ -102,3 +102,65 @@ def test_replace_multiple_overlap(self):
replacements = OrderedDict(reversed(replacement_list))
result = replace_multiple('asdf', replacements)
self.assertEqual(result, '2')
+
+
+class ToNextMultipleTests(unittest.TestCase):
+ def test_to_next_multiple(self):
+ from qupulse.utils.types import TimeType
+ from qupulse.expressions import ExpressionScalar
+
+ duration = TimeType.from_float(47.1415926535)
+ evaluated = to_next_multiple(sample_rate=TimeType.from_float(2.4),quantum=16)(duration)
+ expected = ExpressionScalar('160/3')
+ self.assertEqual(evaluated, expected)
+
+ duration = TimeType.from_float(3.1415926535)
+ evaluated = to_next_multiple(sample_rate=TimeType.from_float(2.4),quantum=16,min_quanta=13)(duration)
+ expected = ExpressionScalar('260/3')
+ self.assertEqual(evaluated, expected)
+
+ duration = 6185240.0000001
+ evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=13)(duration).evaluate_numeric()
+ expected = 6185248
+ self.assertEqual(evaluated, expected)
+
+ duration = 63.99
+ evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=4)(duration).evaluate_numeric()
+ expected = 64
+ self.assertEqual(evaluated, expected)
+
+ duration = 64.01
+ evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=4)(duration).evaluate_numeric()
+ expected = 80
+ self.assertEqual(evaluated, expected)
+
+ duration = 0.
+ evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=13)(duration).evaluate_numeric()
+ expected = 0.
+ self.assertEqual(evaluated, expected)
+
+ duration = ExpressionScalar('abc')
+ evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=13)(duration).evaluate_in_scope(dict(abc=0.))
+ expected = 0.
+ self.assertEqual(evaluated, expected)
+
+ duration = ExpressionScalar('q')
+ evaluated = to_next_multiple(sample_rate=ExpressionScalar('w'),quantum=16,min_quanta=1)(duration).evaluate_in_scope(
+ dict(q=3.14159,w=1.0))
+ expected = 16.
+ self.assertEqual(evaluated, expected)
+
+
+def test_to_next_multiple_padding_duration_evaluation(benchmark):
+ # reminder how to manually run pytest tests:
+ # use pytest -k test_to_next_multiple_padding_duration_evaluation
+ # or for faster collection phase
+ # pytest -k test_to_next_multiple_padding_duration_evaluation tests/utils/utils_tests.py
+
+ from qupulse.pulses import FunctionPT
+ pt = FunctionPT('start+t/t_gate*(end-start)', 't_gate', 'a')
+
+ def padding():
+ pt.pad_to(to_next_multiple(2.4, 16, 4)).duration.evaluate_in_scope({'t_gate': 10.})
+
+ benchmark(padding)