├── .github └── workflows │ ├── pythonpublish.yml │ ├── pythontest.yaml │ └── unittest_publish.yaml ├── .gitignore ├── .zenodo.json ├── LICENSES └── GPL-3.0-or-later.txt ├── README.md ├── RELEASE_NOTES.rst ├── changes.d ├── .gitignore ├── 578.feature ├── 808.doc ├── 835.removal ├── 841.removal ├── 845.removal └── 853.misc ├── coverage.ini ├── doc ├── README.md └── source │ ├── _static │ ├── example_pulse.png │ └── example_pulse.svg │ ├── _templates │ └── autosummary │ │ └── package.rst │ ├── concepts │ ├── awgs.rst │ ├── concepts.rst │ ├── instantiating.rst │ ├── program.rst │ ├── pulsetemplates.rst │ └── serialization.rst │ ├── conf.py │ ├── examples │ ├── 00AbstractPulseTemplate.ipynb │ ├── 00AdvancedTablePulse.ipynb │ ├── 00ArithmeticWithPulseTemplates.ipynb │ ├── 00ComposedPulses.ipynb │ ├── 00ConstantPulseTemplate.ipynb │ ├── 00FunctionPulse.ipynb │ ├── 00MappingTemplate.ipynb │ ├── 00MultiChannelTemplates.ipynb │ ├── 00PointPulse.ipynb │ ├── 00RetrospectiveConstantChannelAddition.ipynb │ ├── 00SimpleTablePulse.ipynb │ ├── 00TimeReversal.ipynb │ ├── 01Measurements.ipynb │ ├── 01ParameterConstraints.ipynb │ ├── 01PulseStorage.ipynb │ ├── 02CreatePrograms.ipynb │ ├── 03FreeInductionDecayExample.ipynb │ ├── 03GateConfigurationExample.ipynb │ ├── 03SnakeChargeScan.ipynb │ ├── 04DynamicNuclearPolarisation.ipynb │ ├── 04ZurichInstrumentsSetup.ipynb │ ├── _HardwareSetup.ipynb │ ├── _LegacySerialization.ipynb │ ├── _TestMeasurement_nested_loops.ipynb │ ├── examples.rst │ ├── hardware │ │ ├── tabor.py │ │ └── zhinst.py │ ├── img │ │ ├── example_pulse.png │ │ ├── example_pulse.svg │ │ ├── gate_pulse_scheme.png │ │ ├── gate_pulse_scheme.svg │ │ ├── sequencing_walkthrough.svg │ │ ├── walkthrough1_01.png │ │ ├── walkthrough1_02.png │ │ ├── walkthrough2_01.png │ │ ├── walkthrough2_02.png │ │ ├── walkthrough2_03.png │ │ ├── walkthrough2_04.png │ │ └── walkthrough2_05.png │ ├── legacy_serialized_pulses │ │ ├── main.json │ │ ├── nested_loops.json │ │ ├── sequence_embedded.json │ │ ├── sequence_referenced.json │ │ ├── stored_template.json │ │ └── table_template.json │ ├── parameters │ │ └── free_induction_decay.json │ └── serialized_pulses │ │ ├── S_init.json │ │ ├── adprep.json │ │ ├── adread.json │ │ ├── free_induction_decay.json │ │ └── my_other_pulse.json │ ├── index.rst │ └── learners_guide.rst ├── pyproject.toml ├── qupulse ├── __init__.py ├── __init__.pyi ├── _program │ ├── __init__.py │ ├── _loop.py │ ├── tabor.py │ ├── transformation.py │ ├── volatile.py │ └── waveforms.py ├── comparable.py ├── expressions │ ├── __init__.py │ ├── protocol.py │ ├── sympy.py │ └── wrapper.py ├── hardware │ ├── __init__.py │ ├── awgs │ │ ├── __init__.py │ │ ├── base.py │ │ ├── dummy.py │ │ ├── tabor.py │ │ ├── tektronix.py │ │ └── zihdawg.py │ ├── dacs │ │ ├── __init__.py │ │ ├── alazar.py │ │ ├── alazar2.py │ │ ├── dac_base.py │ │ └── dummy.py │ ├── feature_awg │ │ ├── __init__.py │ │ ├── base.py │ │ ├── base_features.py │ │ ├── channel_tuple_wrapper.py │ │ ├── features.py │ │ └── tabor.py │ ├── setup.py │ └── util.py ├── parameter_scope.py ├── plotting.py ├── program │ ├── __init__.py │ ├── linspace.py │ ├── loop.py │ ├── transformation.py │ ├── volatile.py │ └── waveforms.py ├── pulses │ ├── __init__.py │ ├── abstract_pulse_template.py │ ├── arithmetic_pulse_template.py │ ├── constant_pulse_template.py │ ├── function_pulse_template.py │ ├── interpolation.py │ ├── loop_pulse_template.py │ ├── mapping_pulse_template.py │ ├── measurement.py │ ├── multi_channel_pulse_template.py │ ├── parameters.py │ ├── plotting.py │ ├── point_pulse_template.py │ ├── pulse_template.py │ ├── pulse_template_parameter_mapping.py │ ├── range.py │ ├── repetition_pulse_template.py │ ├── sequence_pulse_template.py │ ├── table_pulse_template.py │ └── time_reversal_pulse_template.py ├── serialization.py └── utils │ ├── __init__.py │ ├── numeric.py │ ├── performance.py │ ├── sympy.py │ ├── tree.py │ └── types.py ├── readthedocs.yml └── tests ├── __init__.py ├── _program ├── __init__.py ├── loop_tests.py ├── tabor_tests.py ├── transformation_tests.py └── waveforms_tests.py ├── backward_compatibility ├── __init__.py ├── charge_scan_1 │ ├── binary_program_validation.json │ ├── binary_program_validation.py │ ├── channel_mapping.json │ ├── hdawg_preparation_commands.json │ ├── measurement_mapping.json │ ├── parameters.json │ ├── pulse_storage │ │ └── charge_scan.json │ ├── pulse_storage_converted_2018 │ │ └── charge_scan.json │ └── tabor_preparation_commands.json ├── hardware_test_helper.py ├── tabor_backward_compatibility_tests.py └── zhinst_charge_scan_tests.py ├── comparable_tests.py ├── expressions ├── __init__.py └── expression_tests.py ├── hardware ├── __init__.py ├── alazar_tests.py ├── base_tests.py ├── dummy_devices.py ├── dummy_modules.py ├── feature_awg │ ├── __init__.py │ ├── awg_new_driver_base_tests.py │ ├── channel_tuple_wrapper_tests.py │ ├── tabor_new_driver_clock_tests.py │ ├── tabor_new_driver_exex_test.py │ ├── tabor_new_driver_simulator_based_tests.py │ └── tabor_new_driver_tests.py ├── setup_tests.py ├── tabor_clock_tests.py ├── tabor_dummy_based_tests.py ├── tabor_exex_test.py ├── tabor_simulator_based_tests.py ├── tabor_tests.py ├── tektronix_tests.py └── util_tests.py ├── parameter_scope_tests.py ├── program └── linspace_tests.py ├── pulses ├── __init__.py ├── abstract_pulse_template_tests.py ├── arithmetic_pulse_template_tests.py ├── bug_tests.py ├── constant_pulse_template_tests.py ├── function_pulse_tests.py ├── interpolation_tests.py ├── loop_pulse_template_tests.py ├── mapping_pulse_template_tests.py ├── measurement_tests.py ├── multi_channel_pulse_template_tests.py ├── parameters_tests.py ├── plotting_tests.py ├── point_pulse_template_tests.py ├── pulse_template_parameter_mapping_tests.py ├── pulse_template_tests.py ├── repetition_pulse_template_tests.py ├── sequence_pulse_template_tests.py ├── sequencing_dummies.py ├── table_pulse_template_tests.py └── time_reversal_pulse_template_tests.py ├── serialization_dummies.py ├── serialization_tests.py └── utils ├── __init__.py ├── numeric_tests.py ├── performance_tests.py ├── sympy_tests.py ├── time_type_tests.py ├── tree_tests.py ├── types_tests.py └── utils_tests.py /.github/workflows/pythonpublish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Set up Python 13 | uses: actions/setup-python@v5 14 | with: 15 | python-version: '3.10' 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | python -m pip install --upgrade build twine 20 | - name: Build and publish 21 | env: 22 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 23 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 24 | run: | 25 | python -m build 26 | python -m twine upload dist/* 27 | -------------------------------------------------------------------------------- /.github/workflows/pythontest.yaml: -------------------------------------------------------------------------------- 1 | name: Pytest and coveralls 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | - synchronize 10 | branches: 11 | - '**' 12 | paths: 13 | - 'qupulse/**y' 14 | - 'tests/**' 15 | - 'setup.*' 16 | - 'pyproject.toml' 17 | - '.github/workflows/*' 18 | 19 | jobs: 20 | test: 21 | runs-on: ubuntu-latest 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | python-version: ["3.10", "3.11", "3.12"] 26 | numpy-version: [">=1.24,<2.0", ">=2.0"] 27 | env: 28 | INSTALL_EXTRAS: tests,plotting,zurich-instruments,tektronix,tabor-instruments 29 | 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | 35 | - name: Set up Python ${{ matrix.python-version }} 36 | uses: actions/setup-python@v5 37 | with: 38 | python-version: ${{ matrix.python-version }} 39 | cache: pip 40 | 41 | - name: Install dependencies 42 | run: | 43 | python -m pip install --upgrade pip 44 | python -m pip install coverage coveralls 45 | 46 | - name: Install numpy ${{ matrix.numpy-version }} 47 | run: python -m pip install "numpy${{ matrix.numpy-version }}" 48 | 49 | - name: Install package 50 | run: | 51 | python -m pip install .[${{ env.INSTALL_EXTRAS }}] 52 | 53 | - name: Test with pytest 54 | run: | 55 | coverage run -m pytest --junit-xml pytest.xml 56 | 57 | - name: Generate valid name 58 | run: | 59 | numpy_version="${{ matrix.numpy-version }}" 60 | if [[ $numpy_version == *"<2"* ]]; then 61 | numpy_version="1" 62 | else 63 | numpy_version="2" 64 | fi 65 | MATRIX_NAME="python-${{ matrix.python-version }}-numpy-"$numpy_version 66 | echo "MATRIX_NAME=$MATRIX_NAME" >> $GITHUB_ENV 67 | 68 | - name: Upload coverage data to coveralls.io 69 | run: coveralls --service=github 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | COVERALLS_FLAG_NAME: ${{ env.MATRIX_NAME }} 73 | COVERALLS_PARALLEL: true 74 | 75 | - name: Upload Test Results 76 | if: always() 77 | uses: actions/upload-artifact@v4 78 | with: 79 | name: Unit Test Results ( ${{ env.MATRIX_NAME }} ) 80 | path: | 81 | pytest.xml 82 | 83 | coveralls: 84 | name: Indicate completion to coveralls.io 85 | needs: test 86 | runs-on: ubuntu-latest 87 | container: python:3-slim 88 | steps: 89 | - name: Finished 90 | run: | 91 | pip3 install --upgrade coveralls 92 | coveralls --service=github --finish 93 | env: 94 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 95 | 96 | event_file: 97 | name: "Event File" 98 | runs-on: ubuntu-latest 99 | steps: 100 | - name: Upload 101 | uses: actions/upload-artifact@v4 102 | with: 103 | name: Event File 104 | path: ${{ github.event_path }} 105 | -------------------------------------------------------------------------------- /.github/workflows/unittest_publish.yaml: -------------------------------------------------------------------------------- 1 | name: Unit Test Results 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Pytest and coveralls"] 6 | types: 7 | - completed 8 | permissions: {} 9 | 10 | jobs: 11 | test-results: 12 | name: Test Results 13 | runs-on: ubuntu-latest 14 | if: github.event.workflow_run.conclusion != 'skipped' 15 | 16 | permissions: 17 | checks: write 18 | 19 | # needed unless run with comment_mode: off 20 | pull-requests: write 21 | 22 | # required by download step to access artifacts API 23 | actions: read 24 | 25 | steps: 26 | - name: Download and Extract Artifacts 27 | env: 28 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 29 | run: | 30 | mkdir -p artifacts && cd artifacts 31 | 32 | artifacts_url=${{ github.event.workflow_run.artifacts_url }} 33 | 34 | gh api "$artifacts_url" -q '.artifacts[] | [.name, .archive_download_url] | @tsv' | while read artifact 35 | do 36 | IFS=$'\t' read name url <<< "$artifact" 37 | gh api $url > "$name.zip" 38 | unzip -d "$name" "$name.zip" 39 | done 40 | 41 | - name: Publish Test Results 42 | uses: EnricoMi/publish-unit-test-result-action@v2 43 | with: 44 | commit: ${{ github.event.workflow_run.head_sha }} 45 | event_file: artifacts/Event File/event.json 46 | event_name: ${{ github.event.workflow_run.event }} 47 | junit_files: "artifacts/**/*.xml" 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *__pycache__* 3 | doc/build/* 4 | .*project 5 | .eggs* 6 | *.egg-info* 7 | build/* 8 | dist/* 9 | doc/source/examples/.ipynb_checkpoints/* 10 | **.asv 11 | *.orig 12 | /doc/source/_autosummary/* 13 | .idea/ 14 | .mypy_cache/* 15 | tests/hardware/WX2184C.exe 16 | .vscode/* 17 | -------------------------------------------------------------------------------- /.zenodo.json: -------------------------------------------------------------------------------- 1 | { 2 | "creators": [ 3 | { 4 | "orcid": "0000-0002-9399-1055", 5 | "affiliation": "RWTH Aachen University", 6 | "name": "Humpohl, Simon" 7 | }, 8 | { 9 | "orcid": "0000-0001-8678-961X", 10 | "affiliation": "RWTH Aachen University", 11 | "name": "Prediger, Lukas" 12 | }, 13 | { 14 | "orcid": "0000-0002-8227-4018", 15 | "affiliation": "RWTH Aachen University", 16 | "name": "Cerfontaine, Pascal" 17 | }, 18 | { 19 | "affiliation": "Forschungszentrum Jülich", 20 | "name": "Papajewski, Benjamin" 21 | }, 22 | { 23 | "orcid": "0000-0001-9927-3102", 24 | "affiliation": "RWTH Aachen University", 25 | "name": "Bethke, Patrick" 26 | }, 27 | { 28 | "orcid": "0000-0003-2057-9913", 29 | "affiliation": "Forschungszentrum Jülich", 30 | "name": "Lankes, Lukas" 31 | }, 32 | { 33 | "orcid": "0009-0006-9702-2979", 34 | "affiliation": "Forschungszentrum Jülich", 35 | "name": "Willmes, Alexander" 36 | }, 37 | { 38 | "orcid": "0009-0000-3779-4711", 39 | "affiliation": "Forschungszentrum Jülich", 40 | "name": "Kammerloher, Eugen" 41 | } 42 | ], 43 | 44 | "contributors": [ 45 | { 46 | "orcid": "0000-0001-7018-1124", 47 | "affiliation": "Netherlands Organisation for Applied Scientific Research TNO", 48 | "name": "Eendebak, Pieter Thijs" 49 | }, 50 | { 51 | "name": "Kreutz, Maike", 52 | "affiliation": "RWTH Aachen University" 53 | }, 54 | { 55 | "name": "Xue, Ran", 56 | "affiliation": "RWTH Aachen University", 57 | "orcid": "0000-0002-2009-6279" 58 | } 59 | ], 60 | 61 | "related_identifiers": [ 62 | { 63 | "identifier": "2128/24264", 64 | "relation": "isDocumentedBy", 65 | "resource_type": "publication-thesis" 66 | } 67 | ], 68 | 69 | "license": "GPL-3.0-or-later", 70 | 71 | "title": "qupulse: A Quantum compUting PULse parametrization and SEquencing framework", 72 | 73 | "keywords": ["quantum computing", "control pulse"] 74 | } 75 | -------------------------------------------------------------------------------- /changes.d/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | -------------------------------------------------------------------------------- /changes.d/578.feature: -------------------------------------------------------------------------------- 1 | Add a ``to_single_waveform`` keyword argument to ``SequencePT``, ``RepetitionPT``, ``ForLoopPT`` and ``MappingPT``. 2 | When ``to_single_waveform='always'`` is passed the corresponding pulse template is translated into a single waveform on program creation. 3 | -------------------------------------------------------------------------------- /changes.d/808.doc: -------------------------------------------------------------------------------- 1 | Add an example with a Zurich Instruments HDAWG and MFLI. -------------------------------------------------------------------------------- /changes.d/835.removal: -------------------------------------------------------------------------------- 1 | Remove python 3.8 and 3.9 support. Version 3.10 is now the minimal supported version. -------------------------------------------------------------------------------- /changes.d/841.removal: -------------------------------------------------------------------------------- 1 | Remove MATLAB code and the qctoolkit alias for qupulse. 2 | -------------------------------------------------------------------------------- /changes.d/845.removal: -------------------------------------------------------------------------------- 1 | Fallback for a missing `gmpy2` via `fractions` was removed. -------------------------------------------------------------------------------- /changes.d/853.misc: -------------------------------------------------------------------------------- 1 | Remove private and unused frozendict fallback implementations `_FrozenDictByInheritance` and `_FrozenDictByWrapping`. -------------------------------------------------------------------------------- /coverage.ini: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | omit = 4 | *main.py 5 | *__init__.py 6 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # qupulse: A Quantum compUting PULse parametrization and SEquencing framework - Documentation 2 | 3 | This folder contains texts, configuration and scripts which are used to compile the documentation using [sphinx](http://www.sphinx-doc.org/en/stable/). It also contains usage examples for qupulse. 4 | You may either build the documentation yourself or read it on [readthedocs](http://qc-toolkit.readthedocs.org/).[![Documentation Status](https://readthedocs.org/projects/qc-toolkit/badge/?version=latest)](http://qc-toolkit.readthedocs.org/en/latest/?badge=latest) 5 | 6 | 7 | ## Examples 8 | 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*. 9 | 10 | ## Building the Documentation 11 | 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. 12 | 13 | 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/`. 14 | -------------------------------------------------------------------------------- /doc/source/_static/example_pulse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qutech/qupulse/15d67075362c3a056b942f8feb0818f48a5cc1cb/doc/source/_static/example_pulse.png -------------------------------------------------------------------------------- /doc/source/_templates/autosummary/package.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline }} 2 | 3 | {% block modules %} 4 | {% if modules %} 5 | .. rubric:: Modules 6 | 7 | .. autosummary:: 8 | :toctree: 9 | :recursive: 10 | {% for item in modules %} 11 | {{ fullname }}.{{ item }} 12 | {%- endfor %} 13 | {% endif %} 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /doc/source/concepts/awgs.rst: -------------------------------------------------------------------------------- 1 | .. _awgs: 2 | 3 | How qupulse models AWGs 4 | ----------------------- 5 | 6 | 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`. 7 | 8 | When a program is uploaded to an arbitrary waveform generator (AWG) it needs to brought in a form that the hardware 9 | understands. 10 | Most AWGs consist of three significant parts: 11 | 12 | * The actual digital to analog converter (DAC) that outputs samples at a (semi-) fixed rate [1]_, 13 | * a sequencer which tells the DAC what to do, 14 | * waveform memory which contains sampled waveforms in a format that the DAC understands. 15 | 16 | The sequencer feeds the data from the waveform memory to the DAC in the correct order. 17 | Uploading a qupulse pulse to an AWG requires to sample the program, upload waveforms to the memory 18 | and program the sequencer. 19 | 20 | The interface exposed by the vendor to program the sequencer reaches from a simple table like for 21 | Tektronix' AWG5000 series to some kind of complex domain specific language (DSL) like Zurich Instrument' sequencing C. 22 | 23 | Basically all AWGs have some kind of limitations regarding the length of the waveform samples which is often of the 24 | form :math:`n_{\texttt{samples}} = n_{\texttt{min}} + m \cdot n_{\texttt{div}}` with the minimal number of samples 25 | :math:`n_{\texttt{min}}` and some divisor :math:`n_{\texttt{div}}`. 26 | 27 | .. topic:: Implementation detail (might be outdated) 28 | 29 | Holding a voltage for a long time was often best accomplished by repeating a waveform of :math:`n_{\texttt{min}}` to save waveform memory. 30 | Earlier versions of qupulse required you to write your pulse in this way i.e. with a ``RepetitionPT``. 31 | 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. 32 | 33 | .. [1] Some AWGs like the HDAWG can be programmed change the sample rate to a divisor of the "main" rate dynamically. 34 | -------------------------------------------------------------------------------- /doc/source/concepts/concepts.rst: -------------------------------------------------------------------------------- 1 | Concepts 2 | ======== 3 | 4 | This section explains the fundamental design concepts of qupulse. 5 | 6 | .. toctree:: 7 | pulsetemplates 8 | serialization 9 | instantiating 10 | program 11 | awgs 12 | 13 | -------------------------------------------------------------------------------- /doc/source/concepts/instantiating.rst: -------------------------------------------------------------------------------- 1 | .. _instantiating: 2 | 3 | Pulse Instantiation 4 | ------------------- 5 | 6 | As already briefly mentioned in :ref:`pulsetemplates`, instantiation of pulses is the process of obtaining a hardware 7 | interpretable representation of a concrete pulse ready for execution from the quite high-level :class:`.PulseTemplate` 8 | object tree structure that defines parameterizable pulses in qupulse. 9 | 10 | The entry point is the :meth:`.PulseTemplate.create_program` method of the :class:`.PulseTemplate` hierarchy. 11 | It accepts the pulse parameters, and allows to rename and/or omit channels or measurements. 12 | It checks that the provided parameters and mappings are consistent and meet the optionally defined parameter constraints of the pulse template. 13 | The translation target is defined by the :class:`.ProgramBuilder` argument. 14 | 15 | Each pulse template knows what program builder methods to call to translate itself. 16 | 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. 17 | The resulting program is completely backend dependent. 18 | 19 | **Historically**, there was only a single program type :class:`.Loop` which is still the default output type. 20 | 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. 21 | The following describes handling of :class:`.Loop` object only via the :class:`qupulse.hardware.HardwareSetup`. 22 | 23 | The :class:`.Loop` class was designed as a hardware-independent pulse program tree for waveform table based sequencers. 24 | Therefore, the translation into a hardware specific format is a two-step process which consists of the loop object creation as a first step 25 | and the transformation of that tree according to the needs of the hardware as a second step. 26 | However, the AWGs became more flexibly programmable over the years as discussed in :ref:`awgs`. 27 | 28 | 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. 29 | 30 | The second step of the instantiation is performed by the hardware backend and transparent to the user. Upon registering 31 | the pulse with the hardware backend via :meth:`qupulse.hardware.HardwareSetup.register_program`, the backend will determine which 32 | hardware device is responsible for the channels defined in the pulse and delegate the :class:`.Loop` object to the 33 | corresponding device driver. The driver will then sample the pulse waveforms with its configured sample rate, flatten 34 | the program tree if required by the device and, finally, program the device and upload the sampled waveforms. 35 | 36 | The flattening is device dependent because different devices allow for different levels of nested sequences and loops. 37 | 38 | For example the Tabor Electronics WX2184C AWG supports two-fold nesting: waveforms into level-1 sequences, level-1 sequences 39 | into level-2 sequences. In consequence, the program tree is flattened to depth two, i.e., for all tree paths of 40 | larger depth, loops are unrolled and sequences of waveforms are merged into a single waveform until the target depth 41 | is reached. Additionally, the AWG requires waveforms to have a minimal length. Any waveform that is shorter is merged 42 | by the driver with its neighbors in the execution sequence until the minimum waveform length is reached. Further 43 | optimizations and merges (or splits) of waveforms for performance are also possible. 44 | 45 | In contrast, the Zurich Instruments HDAWG allows arbitrary nesting levels and is only limited by the instruction cache. 46 | However, this device supports increment commands which allow the efficient representation of linear voltage sweeps which is **not** possible with the :class:`.Loop` class. 47 | 48 | The section :ref:`program` touches the ideas behind the current program implementations i.e. :class:`.Loop` and :class:`.LinSpaceProgram`. 49 | -------------------------------------------------------------------------------- /doc/source/concepts/program.rst: -------------------------------------------------------------------------------- 1 | .. _program: 2 | 3 | Instantiated Pulse: Program 4 | --------------------------- 5 | 6 | In qupulse an instantiated pulse template is called a program as it is something that an arbitrary waveform generator (AWG) can execute/playback. 7 | 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`. 8 | The dimensions are named by the channel names. 9 | 10 | Programs are created by the :meth:`~.PulseTemplate.create_program` method of `PulseTemplate` which returns a hardware independent and un-parameterized representation. 11 | The method takes a ``program_builder`` keyword argument that is propagated through the pulse template tree and thereby implements the visitor pattern. 12 | 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. 13 | 14 | The :class:`.Loop` default program is the root node of a tree of loop objects of arbitrary depth. 15 | Each node consists of a repetition count and either a waveform or a sequence of nodes which are repeated that many times. 16 | Iterations like the :class:`.ForLoopPT` cannot be represented natively but are unrolled into a sequence of items. 17 | 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. 18 | 19 | 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`. 20 | The main complexity of this program class is the efficient handling of interleaved constant points. 21 | 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. 22 | This allows the efficient encoding of 23 | 24 | .. code:: python 25 | 26 | for idx in range(10): 27 | set_voltage(CONSTANT) # No dependencies 28 | set_voltage(OFFSET + idx * FACTOR) # depends on idx with 29 | 30 | for _ in range(10): # loop 31 | set_voltage(CONSTANT, key=None) 32 | increment_by(FACTOR, key=(FACTOR,)) 33 | 34 | The motivation is that increment commands with this capability are available in the HDAWG command table. 35 | -------------------------------------------------------------------------------- /doc/source/examples/00AbstractPulseTemplate.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "collapsed": true 7 | }, 8 | "source": [ 9 | "# Abstract Pulse Template\n", 10 | "This pulse template can be used as a place holder for a pulse template with a defined interface. Pulse template properties like `defined_channels` can be passed on initialization to declare those properties who make up the interface. Omitted properties raise an `NotSpecifiedError` exception if accessed. Properties which have been accessed are marked as \"frozen\".\n", 11 | "The abstract pulse template can be linked to another pulse template by calling the `link_to` member. The target has to have the same properties for all properties marked as \"frozen\". This ensures a property always returns the same value." 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "from qupulse.pulses import AbstractPT, FunctionPT, AtomicMultiChannelPT, PointPT\n", 21 | "\n", 22 | "init = PointPT([(0, (1, 0)), ('t_init', (0, 1), 'linear')], ['X', 'Y'])\n", 23 | "abstract_readout = AbstractPT('readout', defined_channels={'X', 'Y'}, integral={'X': 1, 'Y': 'a*b'})\n", 24 | "manip = AtomicMultiChannelPT(FunctionPT('sin(t)', 't_manip', channel='X'),\n", 25 | " FunctionPT('cos(t)', 't_manip', channel='Y'))\n", 26 | "\n", 27 | "experiment = init @ manip @ abstract_readout" 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "metadata": {}, 33 | "source": [ 34 | "We can access declared properties like integral. If we try to get a non-declared property an exception is raised." 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": 2, 40 | "metadata": {}, 41 | "outputs": [ 42 | { 43 | "name": "stdout", 44 | "output_type": "stream", 45 | "text": [ 46 | "The integral has been declared so we can get it\n", 47 | "{'X': ExpressionScalar('t_init/2 - cos(t_manip) + 2'), 'Y': ExpressionScalar('a*b + t_init/2 + sin(t_manip)')}\n", 48 | "\n", 49 | "We get an error that for the pulse \"readout\" the property \"duration\" was not specified:\n", 50 | "NotSpecifiedError('readout', 'duration')\n" 51 | ] 52 | } 53 | ], 54 | "source": [ 55 | "print('The integral has been declared so we can get it')\n", 56 | "print(experiment.integral)\n", 57 | "print()\n", 58 | "\n", 59 | "import traceback\n", 60 | "try:\n", 61 | " experiment.duration\n", 62 | "except Exception as err:\n", 63 | " print('We get an error that for the pulse \"readout\" the property \"duration\" was not specified:')\n", 64 | " print(repr(err))" 65 | ] 66 | }, 67 | { 68 | "cell_type": "markdown", 69 | "metadata": {}, 70 | "source": [ 71 | "We can link the abstract pulse template to an actual pulse template. By accessing the integral property above we froze it. Linking a pulse with a different property will result in an error." 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": 3, 77 | "metadata": {}, 78 | "outputs": [ 79 | { 80 | "name": "stdout", 81 | "output_type": "stream", 82 | "text": [ 83 | "With wrong integral value:\n", 84 | "RuntimeError('Cannot link to target. Wrong value of property \"integral\"')\n", 85 | "the linking worked. The new experiment has now a defined duration of ExpressionScalar('t_init + t_manip + t_read') .\n" 86 | ] 87 | } 88 | ], 89 | "source": [ 90 | "my_readout_wrong_integral = AtomicMultiChannelPT(FunctionPT('1', 't_read', channel='X'),\n", 91 | " FunctionPT('a*b', 't_read', channel='Y'))\n", 92 | "\n", 93 | "my_readout = AtomicMultiChannelPT(FunctionPT('1 / t_read', 't_read', channel='X'),\n", 94 | " FunctionPT('a*b / t_read', 't_read', channel='Y'))\n", 95 | "\n", 96 | "try:\n", 97 | " print('With wrong integral value:')\n", 98 | " abstract_readout.link_to(my_readout_wrong_integral)\n", 99 | "except Exception as err:\n", 100 | " print(repr(err))\n", 101 | "\n", 102 | "abstract_readout.link_to(my_readout)\n", 103 | "print('the linking worked. The new experiment has now a defined duration of', repr(experiment.duration), '.')" 104 | ] 105 | } 106 | ], 107 | "metadata": { 108 | "language_info": { 109 | "name": "python" 110 | } 111 | }, 112 | "nbformat": 4, 113 | "nbformat_minor": 1 114 | } 115 | -------------------------------------------------------------------------------- /doc/source/examples/01Measurements.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Definition of Measurements\n", 8 | "\n", 9 | "Many pulse templates allow us to declare measurements upon their creation. Each measurement declaration is a tuple that consists of the measurement's name for later identification, the starting time in the pulse template and the measurement's length. The idea behind measurement names is that you can put different types of measurements in one pulse and easily distinguish between the results. qupulse automatically configures the acquisition driver to measure at the defined measurement windows.\n", 10 | "\n", 11 | "The following example creates a pulse template that contains two parameterized measurements named 'M' and 'N':" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [ 19 | { 20 | "name": "stdout", 21 | "output_type": "stream", 22 | "text": [ 23 | "{'N', 'M'}\n", 24 | "[('M', ExpressionScalar(0), ExpressionScalar('t_meas')), ('N', ExpressionScalar(0), ExpressionScalar('t_meas/2'))]\n" 25 | ] 26 | } 27 | ], 28 | "source": [ 29 | "from qupulse.pulses import PointPT\n", 30 | "\n", 31 | "measured_pt = PointPT([(0, 'm'),\n", 32 | " ('t_meas', 'm')],\n", 33 | " channel_names=('RF_X', 'RF_Y'),\n", 34 | " measurements=[('M', 0, 't_meas'), ('N', 0, 't_meas/2')])\n", 35 | "print(measured_pt.measurement_names)\n", 36 | "print(measured_pt.measurement_declarations)" 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "metadata": { 42 | "collapsed": true 43 | }, 44 | "source": [ 45 | "Our pulse template holds a constant voltage level defined by parameter `m` and has a duration defined by parameters `t_meas`. The measurement `M` starts at time `0`, i.e. immediately when the pulse itself starts, and has a duration of `t_meas`, i.e., as long as the pulse itself. The measurement `N` starts at the same time but only lasts for the half duration of the pulse.\n", 46 | "\n", 47 | "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", 48 | "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", 49 | "\n", 50 | "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." 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": 2, 56 | "metadata": {}, 57 | "outputs": [ 58 | { 59 | "name": "stdout", 60 | "output_type": "stream", 61 | "text": [ 62 | "{'N', 'dbz_fid', 'charge_scan'}\n" 63 | ] 64 | } 65 | ], 66 | "source": [ 67 | "from qupulse.pulses import SequencePT\n", 68 | "\n", 69 | "my_complicated_pulse = SequencePT((measured_pt, {'M': 'charge_scan'}),\n", 70 | " (measured_pt, {'M': 'dbz_fid'}))\n", 71 | "print(my_complicated_pulse.measurement_names)" 72 | ] 73 | } 74 | ], 75 | "metadata": { 76 | "language_info": { 77 | "name": "python" 78 | } 79 | }, 80 | "nbformat": 4, 81 | "nbformat_minor": 2 82 | } 83 | -------------------------------------------------------------------------------- /doc/source/examples/04DynamicNuclearPolarisation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Dynamic Nuclear Polarisation/Changing repetition count during runtime\n", 8 | "\n", 9 | "This example demonstrates how to change the repetition count of pulses during runtime. One possible application of changing parameters during runtime is dynamic nuclear polarisation. We will call parameters which are able to change after program creation volatile. Since this example is meant to illustrate how the concept of changing the values of volatile parameter works, we will use simple example pulses.\n", 10 | "\n", 11 | "First we have to connect to the AWG (If you want to run this cell, set `awg_name` and possibly `awg_address` according to the AWG you are using). " 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "from qupulse.hardware.setup import HardwareSetup\n", 21 | "from doc.source.examples.hardware.zhinst import add_to_hardware_setup\n", 22 | "from doc.source.examples.hardware.tabor import add_tabor_to_hardware_setup\n", 23 | "\n", 24 | "awg_name = 'TABOR'\n", 25 | "awg_address = None\n", 26 | "hardware_setup = HardwareSetup()\n", 27 | "\n", 28 | "if awg_name == 'ZI':\n", 29 | " hdawg, channel_pairs = add_to_hardware_setup(hardware_setup, awg_address, name=awg_name)\n", 30 | " used_awg = hdawg.channel_pair_AB\n", 31 | "elif awg_name == 'TABOR':\n", 32 | " teawg, channel_pairs = add_tabor_to_hardware_setup(hardware_setup, tabor_address=awg_address, name=awg_name)\n", 33 | " used_awg = channel_pairs[0]\n", 34 | "else:\n", 35 | " ValueError('Unknown AWG')" 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "metadata": {}, 41 | "source": [ 42 | "As a next step we create our dnp pulse template, with three different pumping schemes: 'minus', 'zero' and 'plus'. In reality these could for example be t-, s- and cs-pumping pulses." 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "from qupulse.pulses import PointPT, RepetitionPT\n", 52 | "\n", 53 | "zero = PointPT([(0, 0), ('t_quant', 0)], ('X', 'Y'))\n", 54 | "minus = PointPT([(0, '-x'), ('t_quant', '-x')], ('X', 'Y'))\n", 55 | "plus = PointPT([(0, 'x'), ('t_quant', 'x')], ('X', 'Y'))\n", 56 | "\n", 57 | "dnp = RepetitionPT(minus, 'n_minus') @ RepetitionPT(zero, 'n_zero') @ RepetitionPT(plus, 'n_plus')" 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "metadata": {}, 63 | "source": [ 64 | "On program creation, we set the parameters and channel mappings of the program as usual. However we want to be able to change how often we repeat each of the pulses dynamically. For that we have to say on program creating which of the parameters are supposed to change during runtime, using the keyword `volatile`." 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": null, 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "sample_rate = used_awg.sample_rate / 10**9\n", 74 | "n_quant = 192\n", 75 | "t_quant = n_quant / sample_rate\n", 76 | "\n", 77 | "dnp_prog = dnp.create_program(parameters=dict(t_quant=float(t_quant), n_minus=3, n_zero=3, n_plus=3, x=0.25),\n", 78 | " channel_mapping={'X': '{}_A'.format(awg_name), 'Y': '{}_B'.format(awg_name)},\n", 79 | " volatile={'n_minus', 'n_zero', 'n_plus'})\n", 80 | "dnp_prog.cleanup()" 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "metadata": {}, 86 | "source": [ 87 | "Now we can upload our program to the AWG and use it as usual." 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": null, 93 | "metadata": {}, 94 | "outputs": [], 95 | "source": [ 96 | "hardware_setup.register_program('dnp', dnp_prog)\n", 97 | "hardware_setup.arm_program('dnp')\n", 98 | "\n", 99 | "used_awg.run_current_program()\n", 100 | "\n", 101 | "print(used_awg._known_programs['dnp'].program.program)" 102 | ] 103 | }, 104 | { 105 | "cell_type": "markdown", 106 | "metadata": {}, 107 | "source": [ 108 | "As expected our pumping pulses are executed 3 times each.\n", 109 | "\n", 110 | "We can now adjust the repetitions of the pulses by simply using the function `update_parameters`. We need to give `update_parameters` the name of the program we want to change and the values to which we want to set certain parameters. Say, next time we run the program we only want to do one zero pulse but 5 plus pulses instead of 3. Then we can simply do:" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": null, 116 | "metadata": {}, 117 | "outputs": [], 118 | "source": [ 119 | "hardware_setup.update_parameters('dnp', dict(n_zero=1, n_plus=5))" 120 | ] 121 | }, 122 | { 123 | "cell_type": "markdown", 124 | "metadata": {}, 125 | "source": [ 126 | "This changes the program in the AWG and the program memory accordingly such that next time we run the program the AWG will output 3 minus, 1 zero and 5 plus pulses." 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": null, 132 | "metadata": {}, 133 | "outputs": [], 134 | "source": [ 135 | "used_awg.run_current_program()\n", 136 | "\n", 137 | "print(used_awg._known_programs['dnp'].program.program)" 138 | ] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "metadata": {}, 143 | "source": [ 144 | "As we can see the AWG now outputs 3 minus pulses, 1 zero pulse and 5 plus pulses as desired." 145 | ] 146 | } 147 | ], 148 | "metadata": { 149 | "language_info": { 150 | "name": "python" 151 | }, 152 | "nbsphinx": { 153 | "execute": "never" 154 | } 155 | }, 156 | "nbformat": 4, 157 | "nbformat_minor": 1 158 | } 159 | -------------------------------------------------------------------------------- /doc/source/examples/examples.rst: -------------------------------------------------------------------------------- 1 | .. _examples: 2 | 3 | Examples 4 | ======== 5 | 6 | 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. 7 | 8 | 9 | .. toctree:: 10 | :caption: Pulse template types 11 | :name: pt_types 12 | 13 | 00SimpleTablePulse 14 | 00AdvancedTablePulse 15 | 00FunctionPulse 16 | 00PointPulse 17 | 00ComposedPulses 18 | 00ConstantPulseTemplate 19 | 00MultiChannelTemplates 20 | 00MappingTemplate 21 | 00AbstractPulseTemplate 22 | 00ArithmeticWithPulseTemplates 23 | 00RetrospectiveConstantChannelAddition 24 | 00TimeReversal 25 | 26 | .. toctree:: 27 | :caption: Pulse template features 28 | :name: pt_feat 29 | 30 | 01PulseStorage 31 | 01Measurements 32 | 01ParameterConstraints 33 | 34 | .. toctree:: 35 | :caption: Physically motivated examples 36 | :name: physical_examples 37 | 38 | 03SnakeChargeScan 39 | 03FreeInductionDecayExample 40 | 03GateConfigurationExample 41 | 04DynamicNuclearPolarisation 42 | 43 | .. toctree:: 44 | :caption: Pulse playback related examples 45 | :name: hardware_examples 46 | 47 | 02CreatePrograms 48 | 04ZurichInstrumentsSetup 49 | 50 | 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. 51 | If you are just learning how to get around in qupulse please ignore them. -------------------------------------------------------------------------------- /doc/source/examples/hardware/tabor.py: -------------------------------------------------------------------------------- 1 | from qupulse.hardware.setup import HardwareSetup, PlaybackChannel, MarkerChannel 2 | from qupulse.hardware.awgs.tabor import TaborChannelPair, TaborAWGRepresentation 3 | import pyvisa 4 | 5 | 6 | def add_tabor_to_hardware_setup(hardware_setup: HardwareSetup, tabor_address: str = None, name: str = 'TABOR'): 7 | def _find_tabor_address(): 8 | known_instruments = pyvisa.ResourceManager().list_resources() 9 | 10 | _tabor_address = None 11 | for address in known_instruments: 12 | if r'0x168C::0x2184' in address: 13 | _tabor_address = address 14 | break 15 | if _tabor_address is None: 16 | raise RuntimeError('Could not locate TaborAWG') 17 | 18 | return _tabor_address 19 | 20 | if tabor_address is None: 21 | tabor_address = _find_tabor_address() 22 | 23 | tawg = TaborAWGRepresentation(tabor_address, reset=True) 24 | 25 | channel_pairs = [] 26 | for pair_name in ('AB', 'CD'): 27 | channel_pair = getattr(tawg, 'channel_pair_%s' % pair_name) 28 | channel_pairs.append(channel_pair) 29 | 30 | for ch_i, ch_name in enumerate(pair_name): 31 | playback_name = '{name}_{ch_name}'.format(name=name, ch_name=ch_name) 32 | hardware_setup.set_channel(playback_name, PlaybackChannel(channel_pair, ch_i)) 33 | hardware_setup.set_channel(playback_name + '_MARKER', MarkerChannel(channel_pair, ch_i)) 34 | 35 | return tawg, channel_pairs 36 | -------------------------------------------------------------------------------- /doc/source/examples/hardware/zhinst.py: -------------------------------------------------------------------------------- 1 | from qupulse.hardware.awgs.zihdawg import HDAWGRepresentation 2 | from qupulse.hardware.setup import HardwareSetup, PlaybackChannel, MarkerChannel 3 | 4 | 5 | def add_to_hardware_setup(hardware_setup: HardwareSetup, serial, name='ZI'): 6 | hdawg = HDAWGRepresentation(serial, 'USB') 7 | 8 | channel_pairs = [] 9 | for pair_name in ('AB', 'CD', 'EF', 'GH'): 10 | channel_pair = getattr(hdawg, 'channel_pair_%s' % pair_name) 11 | 12 | for ch_i, ch_name in enumerate(pair_name): 13 | playback_name = '{name}_{ch_name}'.format(name=name, ch_name=ch_name) 14 | hardware_setup.set_channel(playback_name, 15 | PlaybackChannel(channel_pair, ch_i)) 16 | hardware_setup.set_channel(playback_name + '_MARKER_FRONT', MarkerChannel(channel_pair, 2 * ch_i)) 17 | hardware_setup.set_channel(playback_name + '_MARKER_BACK', MarkerChannel(channel_pair, 2 * ch_i + 1)) 18 | 19 | return hdawg, channel_pairs 20 | -------------------------------------------------------------------------------- /doc/source/examples/img/example_pulse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qutech/qupulse/15d67075362c3a056b942f8feb0818f48a5cc1cb/doc/source/examples/img/example_pulse.png -------------------------------------------------------------------------------- /doc/source/examples/img/gate_pulse_scheme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qutech/qupulse/15d67075362c3a056b942f8feb0818f48a5cc1cb/doc/source/examples/img/gate_pulse_scheme.png -------------------------------------------------------------------------------- /doc/source/examples/img/walkthrough1_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qutech/qupulse/15d67075362c3a056b942f8feb0818f48a5cc1cb/doc/source/examples/img/walkthrough1_01.png -------------------------------------------------------------------------------- /doc/source/examples/img/walkthrough1_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qutech/qupulse/15d67075362c3a056b942f8feb0818f48a5cc1cb/doc/source/examples/img/walkthrough1_02.png -------------------------------------------------------------------------------- /doc/source/examples/img/walkthrough2_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qutech/qupulse/15d67075362c3a056b942f8feb0818f48a5cc1cb/doc/source/examples/img/walkthrough2_01.png -------------------------------------------------------------------------------- /doc/source/examples/img/walkthrough2_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qutech/qupulse/15d67075362c3a056b942f8feb0818f48a5cc1cb/doc/source/examples/img/walkthrough2_02.png -------------------------------------------------------------------------------- /doc/source/examples/img/walkthrough2_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qutech/qupulse/15d67075362c3a056b942f8feb0818f48a5cc1cb/doc/source/examples/img/walkthrough2_03.png -------------------------------------------------------------------------------- /doc/source/examples/img/walkthrough2_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qutech/qupulse/15d67075362c3a056b942f8feb0818f48a5cc1cb/doc/source/examples/img/walkthrough2_04.png -------------------------------------------------------------------------------- /doc/source/examples/img/walkthrough2_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qutech/qupulse/15d67075362c3a056b942f8feb0818f48a5cc1cb/doc/source/examples/img/walkthrough2_05.png -------------------------------------------------------------------------------- /doc/source/examples/legacy_serialized_pulses/main.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": { 3 | "A": [ 4 | [ 5 | "ta", 6 | "va", 7 | "hold" 8 | ], 9 | [ 10 | "tb", 11 | "vb", 12 | "linear" 13 | ], 14 | [ 15 | "tend", 16 | 0, 17 | "jump" 18 | ] 19 | ] 20 | }, 21 | "measurements": [], 22 | "parameter_constraints": [], 23 | "type": "qupulse.pulses.table_pulse_template.TablePulseTemplate" 24 | } -------------------------------------------------------------------------------- /doc/source/examples/legacy_serialized_pulses/sequence_embedded.json: -------------------------------------------------------------------------------- 1 | { 2 | "subtemplates": [ 3 | { 4 | "channel_mapping": { 5 | "A": "A" 6 | }, 7 | "measurement_mapping": {}, 8 | "parameter_mapping": { 9 | "ta": "1", 10 | "tb": "2", 11 | "tend": "5", 12 | "va": "5", 13 | "vb": "0" 14 | }, 15 | "template": { 16 | "entries": { 17 | "A": [ 18 | [ 19 | "ta", 20 | "va", 21 | "hold" 22 | ], 23 | [ 24 | "tb", 25 | "vb", 26 | "linear" 27 | ], 28 | [ 29 | "tend", 30 | 0, 31 | "jump" 32 | ] 33 | ] 34 | }, 35 | "measurements": [], 36 | "parameter_constraints": [], 37 | "type": "qupulse.pulses.table_pulse_template.TablePulseTemplate" 38 | }, 39 | "type": "qupulse.pulses.mapping_pulse_template.MappingPulseTemplate" 40 | } 41 | ], 42 | "type": "qupulse.pulses.sequence_pulse_template.SequencePulseTemplate" 43 | } -------------------------------------------------------------------------------- /doc/source/examples/legacy_serialized_pulses/sequence_referenced.json: -------------------------------------------------------------------------------- 1 | { 2 | "subtemplates": [ 3 | { 4 | "channel_mapping": { 5 | "A": "A" 6 | }, 7 | "measurement_mapping": {}, 8 | "parameter_mapping": { 9 | "ta": "1", 10 | "tb": "2", 11 | "tend": "5", 12 | "va": "5", 13 | "vb": "0" 14 | }, 15 | "template": "table_template", 16 | "type": "qupulse.pulses.mapping_pulse_template.MappingPulseTemplate" 17 | } 18 | ], 19 | "type": "qupulse.pulses.sequence_pulse_template.SequencePulseTemplate" 20 | } -------------------------------------------------------------------------------- /doc/source/examples/legacy_serialized_pulses/stored_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": { 3 | "A": [ 4 | [ 5 | 0, 6 | 0, 7 | "hold" 8 | ], 9 | [ 10 | 4, 11 | 20, 12 | "linear" 13 | ] 14 | ] 15 | }, 16 | "measurements": [], 17 | "parameter_constraints": [], 18 | "type": "qupulse.pulses.table_pulse_template.TablePulseTemplate" 19 | } -------------------------------------------------------------------------------- /doc/source/examples/legacy_serialized_pulses/table_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": { 3 | "A": [ 4 | [ 5 | "ta", 6 | "va", 7 | "hold" 8 | ], 9 | [ 10 | "tb", 11 | "vb", 12 | "linear" 13 | ], 14 | [ 15 | "tend", 16 | 0, 17 | "jump" 18 | ] 19 | ] 20 | }, 21 | "measurements": [], 22 | "parameter_constraints": [], 23 | "type": "qupulse.pulses.table_pulse_template.TablePulseTemplate" 24 | } -------------------------------------------------------------------------------- /doc/source/examples/parameters/free_induction_decay.json: -------------------------------------------------------------------------------- 1 | {"meas": [0, 0], "op": [5, -5], "eps_J": [1, -1], "ST_plus": [2.5, -2.5], "S_init": [-1, -1], "ST_jump": [1, -1], "max_ramp_speed": 0.3, "t_init": 5, "t_meas_wait": 1, "t_ST_prep": 10, "t_op": 20, "t_ST_read": 10, "t_meas_start": 20, "t_meas_duration": 5, "t_start": 5, "t_step": 5, "N_fid_steps": 1, "N_repetitions": 1} -------------------------------------------------------------------------------- /doc/source/examples/serialized_pulses/S_init.json: -------------------------------------------------------------------------------- 1 | { 2 | "#identifier": "S_init", 3 | "#type": "qupulse.pulses.point_pulse_template.PointPulseTemplate", 4 | "channel_names": [ 5 | "RFX", 6 | "RFY" 7 | ], 8 | "time_point_tuple_list": [ 9 | [ 10 | 0, 11 | "S_init", 12 | "hold" 13 | ], 14 | [ 15 | "t_init", 16 | "S_init", 17 | "hold" 18 | ] 19 | ] 20 | } -------------------------------------------------------------------------------- /doc/source/examples/serialized_pulses/adprep.json: -------------------------------------------------------------------------------- 1 | { 2 | "#identifier": "adprep", 3 | "#type": "qupulse.pulses.point_pulse_template.PointPulseTemplate", 4 | "channel_names": [ 5 | "RFX", 6 | "RFY" 7 | ], 8 | "parameter_constraints": [ 9 | "Abs(ST_jump/2 - ST_plus + meas) <= Abs(ST_plus - meas)", 10 | "Abs(ST_jump/2 - ST_plus + meas)/t_ST_prep <= max_ramp_speed", 11 | "Abs(ST_jump/2 + ST_plus - op)/Abs(t_ST_prep - t_op) <= max_ramp_speed" 12 | ], 13 | "time_point_tuple_list": [ 14 | [ 15 | 0, 16 | "meas", 17 | "hold" 18 | ], 19 | [ 20 | "t_ST_prep", 21 | "ST_plus - ST_jump/2", 22 | "linear" 23 | ], 24 | [ 25 | "t_ST_prep", 26 | "ST_plus + ST_jump/2", 27 | "hold" 28 | ], 29 | [ 30 | "t_op", 31 | "op", 32 | "linear" 33 | ] 34 | ] 35 | } -------------------------------------------------------------------------------- /doc/source/examples/serialized_pulses/adread.json: -------------------------------------------------------------------------------- 1 | { 2 | "#identifier": "adread", 3 | "#type": "qupulse.pulses.point_pulse_template.PointPulseTemplate", 4 | "channel_names": [ 5 | "RFX", 6 | "RFY" 7 | ], 8 | "measurements": [ 9 | [ 10 | "m", 11 | "t_meas_start", 12 | "t_meas_duration" 13 | ] 14 | ], 15 | "parameter_constraints": [ 16 | "Abs(ST_jump/2 - ST_plus + meas) <= Abs(ST_plus - meas)", 17 | "Abs(ST_jump/2 - ST_plus + meas)/t_ST_read <= max_ramp_speed", 18 | "Abs(ST_jump/2 + ST_plus - op)/Abs(t_ST_read - t_op) <= max_ramp_speed" 19 | ], 20 | "time_point_tuple_list": [ 21 | [ 22 | 0, 23 | "op", 24 | "hold" 25 | ], 26 | [ 27 | "t_ST_read", 28 | "ST_plus + ST_jump/2", 29 | "linear" 30 | ], 31 | [ 32 | "t_ST_read", 33 | "ST_plus - ST_jump/2", 34 | "hold" 35 | ], 36 | [ 37 | "t_meas_start", 38 | "meas", 39 | "linear" 40 | ], 41 | [ 42 | "t_meas_start + t_meas_duration", 43 | "meas", 44 | "hold" 45 | ] 46 | ] 47 | } -------------------------------------------------------------------------------- /doc/source/examples/serialized_pulses/free_induction_decay.json: -------------------------------------------------------------------------------- 1 | { 2 | "#identifier": "free_induction_decay", 3 | "#type": "qupulse.pulses.repetition_pulse_template.RepetitionPulseTemplate", 4 | "body": { 5 | "#type": "qupulse.pulses.loop_pulse_template.ForLoopPulseTemplate", 6 | "body": { 7 | "#type": "qupulse.pulses.sequence_pulse_template.SequencePulseTemplate", 8 | "subtemplates": [ 9 | { 10 | "#identifier": "S_init", 11 | "#type": "reference" 12 | }, 13 | { 14 | "#type": "qupulse.pulses.point_pulse_template.PointPulseTemplate", 15 | "channel_names": [ 16 | "RFX", 17 | "RFY" 18 | ], 19 | "time_point_tuple_list": [ 20 | [ 21 | 0, 22 | "meas", 23 | "hold" 24 | ], 25 | [ 26 | "t_meas_wait", 27 | "meas", 28 | "hold" 29 | ] 30 | ] 31 | }, 32 | { 33 | "#identifier": "adprep", 34 | "#type": "reference" 35 | }, 36 | { 37 | "#type": "qupulse.pulses.mapping_pulse_template.MappingPulseTemplate", 38 | "channel_mapping": { 39 | "RFX": "RFX", 40 | "RFY": "RFY" 41 | }, 42 | "parameter_mapping": { 43 | "eps_J": "eps_J", 44 | "op": "op", 45 | "t_fid": "t_start + i_fid*t_step" 46 | }, 47 | "template": { 48 | "#type": "qupulse.pulses.point_pulse_template.PointPulseTemplate", 49 | "channel_names": [ 50 | "RFX", 51 | "RFY" 52 | ], 53 | "time_point_tuple_list": [ 54 | [ 55 | 0, 56 | "op-eps_J", 57 | "hold" 58 | ], 59 | [ 60 | "t_fid", 61 | "op-eps_J", 62 | "hold" 63 | ] 64 | ] 65 | } 66 | }, 67 | { 68 | "#identifier": "adread", 69 | "#type": "reference" 70 | } 71 | ] 72 | }, 73 | "loop_index": "i_fid", 74 | "loop_range": [ 75 | 0, 76 | "N_fid_steps", 77 | 1 78 | ] 79 | }, 80 | "repetition_count": "N_repetitions" 81 | } -------------------------------------------------------------------------------- /doc/source/examples/serialized_pulses/my_other_pulse.json: -------------------------------------------------------------------------------- 1 | { 2 | "#identifier": "my_other_pulse", 3 | "#type": "qupulse.pulses.function_pulse_template.FunctionPulseTemplate", 4 | "channel": "default", 5 | "duration_expression": "2*pi/omega", 6 | "expression": "sin(omega*t)", 7 | "measurements": [], 8 | "parameter_constraints": [] 9 | } -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. qupulse documentation master file, created by 2 | sphinx-quickstart on Mon Aug 10 09:57:22 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to qupulse's documentation! 7 | ====================================== 8 | 9 | ``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``. 10 | 11 | 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. 12 | 13 | There is a :ref:`learners guide ` available to help with an efficient exploration of qupulse's features. 14 | 15 | Contents: 16 | 17 | .. toctree:: 18 | :maxdepth: 4 19 | :numbered: 20 | 21 | concepts/concepts 22 | examples/examples 23 | _autosummary/qupulse 24 | learners_guide 25 | 26 | qupulse API Documentation 27 | ========================= 28 | 29 | .. autosummary:: 30 | :recursive: 31 | :toctree: _autosummary 32 | :template: autosummary/package.rst 33 | 34 | qupulse 35 | 36 | .. qupulse API Documentation 37 | 38 | Indices and tables 39 | ================== 40 | 41 | * :ref:`genindex` 42 | * :ref:`modindex` 43 | * :ref:`search` 44 | 45 | -------------------------------------------------------------------------------- /doc/source/learners_guide.rst: -------------------------------------------------------------------------------- 1 | .. _learners_guide: 2 | 3 | Learners Guide - writing pulses with qupulse 4 | -------------------------------------------- 5 | 6 | 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. 7 | 8 | The following steps assume that you have qupulse installed and are able to run the example notebooks. 9 | 10 | 11 | Basic pulse writing 12 | ^^^^^^^^^^^^^^^^^^^ 13 | 14 | .. topic:: Info 15 | 16 | **Estimated time:** 17 | 30 minutes for reading 18 | 60 minutes for the examples 19 | 60 minutes for experimenting 20 | 21 | **Target group:** 22 | 23 | **Learning Goals:** The learner is able to define a parameterized nested pulse template. 24 | 25 | **Learning Task 1:** Read the concept section about :ref:`pulsetemplates`. 26 | 27 | **Exercise Task 1:** Go through the following examples that introduce the shipped atomic pulse templates: 28 | 29 | * :ref:`/examples/00SimpleTablePulse.ipynb` 30 | * :ref:`/examples/00AdvancedTablePulse.ipynb` 31 | * :ref:`/examples/00FunctionPulse.ipynb` 32 | * :ref:`/examples/00PointPulse.ipynb` 33 | * :ref:`/examples/00ConstantPulseTemplate.ipynb` 34 | 35 | **Exercise Task 2:** Go through the following examples that introduce the most important composed pulse templates: 36 | 37 | * :ref:`/examples/00ComposedPulses.ipynb` 38 | * :ref:`/examples/00MappingTemplate.ipynb` 39 | * :ref:`/examples/00MultiChannelTemplates.ipynb` 40 | 41 | **Exercise Task 3:** Go through the following examples that introduce other useful pulse templates: 42 | 43 | * :ref:`/examples/00ArithmeticWithPulseTemplates.ipynb` 44 | * :ref:`/examples/00RetrospectiveConstantChannelAddition.ipynb` 45 | * :ref:`/examples/00TimeReversal.ipynb` 46 | 47 | Pulse template features 48 | ^^^^^^^^^^^^^^^^^^^^^^^ 49 | 50 | .. topic:: Info 51 | 52 | **Estimated time:** 53 | 20 minutes for reading 54 | 60 minutes for the examples 55 | 60 minutes for experimenting 56 | 57 | **Target group:** 58 | 59 | **Learning Goals:** The learner to save pulse templates to the file system. 60 | 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. 61 | 62 | 63 | **Learning Task 2:** Read the concept section about :ref:`serialization`. 64 | 65 | **Exercise Task 4:** Go through the :ref:`/examples/01PulseStorage.ipynb` example. It shows how to load and store pulse templates to disk. 66 | 67 | **Exercise Task 5:** Go through the :ref:`/examples/01Measurements.ipynb` example. It shows how to define and inspect measurement windows. 68 | 69 | **Exercise Task 6:** Go through the :ref:`/examples/01ParameterConstraints.ipynb` example. It shows how to use parameter constraints to enforce invariants. 70 | 71 | **Exercise Task 7:** Go through the :ref:`/examples/03SnakeChargeScan.ipynb` example which shows a realistic pulse. 72 | 73 | Hardware capabilities and limitations 74 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 75 | 76 | This section introduces aspects of the hardware that are relevant for every experimenter. 77 | 78 | This section is incomplete. 79 | 80 | .. topic:: Info 81 | 82 | **Estimated time:** 83 | 20 minutes for reading 84 | 85 | **Target group:** People who want to use qupulse in an experiment. 86 | 87 | **Learning Goals:** 88 | The learner can identify if a hardware limitation related exception that is raised is due to an error on their end and mitigate it. 89 | The learner understands capabilities of at least one type of AWGs. 90 | 91 | 92 | **Learning Task 1:** 93 | 94 | Read :ref:`program` and :ref:`awgs`. 95 | 96 | 97 | Setup an experiment 98 | ^^^^^^^^^^^^^^^^^^^ 99 | 100 | 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`. 101 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "qupulse" 7 | dynamic = ["version"] 8 | description = "A Quantum compUting PULse parametrization and SEquencing framework" 9 | readme = "README.md" 10 | license = "GPL-3.0-or-later" 11 | requires-python = ">=3.10" 12 | authors = [ 13 | { name = "Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University" }, 14 | ] 15 | keywords = [ 16 | "control", 17 | "physics", 18 | "pulse", 19 | "quantum", 20 | "qubit", 21 | ] 22 | classifiers = [ 23 | "Intended Audience :: Science/Research", 24 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 25 | "Operating System :: OS Independent", 26 | "Programming Language :: Python :: 3", 27 | "Topic :: Scientific/Engineering", 28 | ] 29 | dependencies = [ 30 | "frozendict", 31 | "lazy_loader", 32 | "numpy", 33 | "sympy>=1.1.1", 34 | # This is required because there is no 3.12 compatible gmpy2 stable release as of 2024.06.20 35 | "gmpy2;python_version<'3.12'", 36 | "gmpy2>=2.2.0rc1;python_version>='3.12'" 37 | ] 38 | 39 | [project.optional-dependencies] 40 | autologging = [ 41 | "autologging", 42 | ] 43 | default = [ 44 | "pandas", 45 | "scipy", 46 | "qupulse[tests,docs,plotting,autologging,faster-sampling]", 47 | ] 48 | docs = [ 49 | "ipykernel", 50 | "nbsphinx", 51 | "pyvisa", 52 | "sphinx>=4", 53 | ] 54 | faster-sampling = [ 55 | "numba", 56 | ] 57 | plotting = [ 58 | "matplotlib", 59 | ] 60 | tabor-instruments = [ 61 | "tabor_control>=0.1.1", 62 | ] 63 | tektronix = [ 64 | "tek_awg>=0.2.1", 65 | ] 66 | tests = [ 67 | "pytest", 68 | "pytest_benchmark", 69 | ] 70 | zurich-instruments = [ 71 | "qupulse-hdawg", 72 | ] 73 | 74 | [project.urls] 75 | Homepage = "https://github.com/qutech/qupulse" 76 | 77 | [tool.hatch.version] 78 | path = "qupulse/__init__.py" 79 | 80 | [tool.hatch.build.targets.sdist] 81 | include = [ 82 | "/qupulse", 83 | ] 84 | 85 | [tool.hatch.envs.default] 86 | features = ["default"] 87 | 88 | [tool.hatch.envs.hatch-test] 89 | template = "default" 90 | 91 | [tool.hatch.envs.docs] 92 | installer = "uv" 93 | dependencies = [ 94 | "qupulse[default,zurich-instruments]", 95 | "sphinx", 96 | "nbsphinx", 97 | "sphinx-rtd-theme" 98 | ] 99 | [tool.hatch.envs.docs.scripts] 100 | # This is a hack to achieve cross-platform version extraction until https://github.com/pypa/hatch/issues/1006 101 | build = """ 102 | python -c "import subprocess, os; \ 103 | result = subprocess.run(['hatch', 'version'], capture_output=True, text=True); \ 104 | version = result.stdout.strip(); \ 105 | 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])" 106 | """ 107 | latex = """ 108 | python -c "import subprocess, os; \ 109 | result = subprocess.run(['hatch', 'version'], capture_output=True, text=True); \ 110 | version = result.stdout.strip(); \ 111 | subprocess.run(['sphinx-build', '-b', 'latex', 'doc/source', 'doc/build/latex', '-D', 'version=%s' % version, '-D', 'release=%s' % version])" 112 | """ 113 | html = """ 114 | python -c "import subprocess, os; \ 115 | result = subprocess.run(['hatch', 'version'], capture_output=True, text=True); \ 116 | version = result.stdout.strip(); \ 117 | subprocess.run(['sphinx-build', '-b', 'html', 'doc/source', 'doc/build/html', '-D', 'version=%s' % version, '-D', 'release=%s' % version])" 118 | """ 119 | clean= """ 120 | python -c "import shutil; shutil.rmtree('doc/build')" 121 | """ 122 | 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" 123 | 124 | [tool.hatch.envs.changelog] 125 | detached = true 126 | dependencies = [ 127 | "towncrier", 128 | ] 129 | 130 | [tool.hatch.envs.changelog.scripts] 131 | draft = "towncrier build --version main --draft" 132 | release = "towncrier build --yes --version {args}" 133 | 134 | [tool.towncrier] 135 | directory = "changes.d" 136 | package = "qupulse" 137 | package_dir = "./qupulse" 138 | filename = "RELEASE_NOTES.rst" 139 | name = "qupulse" 140 | issue_format = "`#{issue} `_" 141 | 142 | [tool.pytest.ini_options] 143 | minversion = "6.0" 144 | python_files = [ 145 | "*_tests.py", 146 | "*_bug.py" 147 | ] 148 | filterwarnings = [ 149 | # syntax is action:message_regex:category:module_regex:lineno 150 | # we fail on all with a whitelist because a dependency might mess-up passing the correct stacklevel 151 | "error::SyntaxWarning", 152 | "error::DeprecationWarning", 153 | # pytest uses readline which uses collections.abc 154 | # "ignore:Using or importing the ABCs from 'collections' instead of from 'collections\.abc\' is deprecated:DeprecationWarning:.*readline.*" 155 | ] 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /qupulse/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | """A Quantum compUting PULse parametrization and SEquencing framework.""" 6 | 7 | import lazy_loader as lazy 8 | 9 | __version__ = '0.10' 10 | 11 | __getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) 12 | 13 | # we explicitly import qupulse to register all deserialization handles 14 | from qupulse import pulses 15 | -------------------------------------------------------------------------------- /qupulse/__init__.pyi: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from . import pulses 6 | from . import hardware 7 | from . import utils 8 | from . import _program 9 | from . import program 10 | 11 | from . import comparable 12 | from . import expressions 13 | from . import parameter_scope 14 | from . import serialization 15 | from . import plotting 16 | 17 | from .utils.types import MeasurementWindow, ChannelID 18 | 19 | __all__ = ['pulses', 20 | 'hardware', 21 | 'utils', 22 | '_program', 23 | 'program', 24 | 'comparable', 25 | 'expressions', 26 | 'parameter_scope', 27 | 'serialization', 28 | 'MeasurementWindow', 29 | 'ChannelID', 30 | 'plotting', 31 | ] 32 | -------------------------------------------------------------------------------- /qupulse/_program/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | """This is a private package meaning there are no stability guarantees. 6 | 7 | Large parts of this package where stabilized and live now in :py:mod:`qupulse.program`. 8 | """ 9 | -------------------------------------------------------------------------------- /qupulse/_program/_loop.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | """Backwards compatibility link to qupulse.program.loop""" 6 | 7 | from qupulse.program.loop import * 8 | 9 | import qupulse.program.loop 10 | 11 | __all__ = qupulse.program.loop.__all__ 12 | 13 | del qupulse 14 | -------------------------------------------------------------------------------- /qupulse/_program/transformation.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from qupulse.program.transformation import * 6 | 7 | import qupulse.program.transformation 8 | 9 | __all__ = qupulse.program.transformation.__all__ 10 | 11 | del qupulse 12 | -------------------------------------------------------------------------------- /qupulse/_program/volatile.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from qupulse.program.volatile import * 6 | 7 | import qupulse.program.volatile 8 | 9 | __all__ = qupulse.program.volatile.__all__ 10 | 11 | del qupulse 12 | -------------------------------------------------------------------------------- /qupulse/_program/waveforms.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | """Backwards compatibility link to qupulse.program.waveforms""" 6 | 7 | from qupulse.program.waveforms import * 8 | 9 | import qupulse.program.waveforms 10 | 11 | __all__ = qupulse.program.waveforms.__all__ 12 | 13 | del qupulse 14 | -------------------------------------------------------------------------------- /qupulse/comparable.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | """This module defines the abstract Comparable class.""" 6 | from abc import abstractmethod 7 | from typing import Hashable, Any 8 | import warnings 9 | 10 | from qupulse.utils.types import DocStringABCMeta 11 | 12 | 13 | __all__ = ["Comparable"] 14 | 15 | 16 | warnings.warn("qupulse.comparable is deprecated since 0.11 and will be removed in 0.12", DeprecationWarning) 17 | 18 | 19 | class Comparable(metaclass=DocStringABCMeta): 20 | """An object that can be queried for equality with other Comparable objects. 21 | 22 | Subclasses must override the abstract property _compare_key which shall provide some object 23 | natively equatable in Python (e.g., strings, numbers, tuples containing those, etc..). 24 | Comparable provides implementations of the hashing function as well as the equals and not-equals 25 | operators based on comparison of this key. 26 | """ 27 | __slots__ = () 28 | 29 | @property 30 | @abstractmethod 31 | def compare_key(self) -> Hashable: 32 | """Return a unique key used in comparison and hashing operations. 33 | 34 | The key must describe the essential properties of the object. 35 | Two objects are equal iff their keys are identical. 36 | """ 37 | 38 | def __hash__(self) -> int: 39 | """Return a hash value of this Comparable object.""" 40 | return hash(self.compare_key) 41 | 42 | def __eq__(self, other: Any) -> bool: 43 | """True, if other is equal to this Comparable object.""" 44 | return isinstance(other, self.__class__) and self.compare_key == other.compare_key 45 | 46 | def __ne__(self, other: Any) -> bool: 47 | """True, if other is not equal to this Comparable object.""" 48 | return not self == other 49 | -------------------------------------------------------------------------------- /qupulse/expressions/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | """This subpackage contains qupulse's expression logic. The submodule :py:mod:`.expressions.protocol` defines the :py:class:`typing.Protocol` 6 | that expression functionality providers must implement. This allows to substitute the powerful and expressive but slow 7 | default implementation with a faster less expressive backend. 8 | 9 | The default implementation is in :py:mod:`.expressions.sympy`. 10 | 11 | There is are wrapper classes for finding non-protocol uses of expression in :py:mod:`.expressions.wrapper`. Define 12 | ``QUPULSE_EXPRESSION_WRAPPER`` environment variable when running python to wrap all expression usages. 13 | """ 14 | 15 | from typing import Type, TypeVar 16 | from numbers import Real 17 | import os 18 | 19 | import numpy as np 20 | import sympy as sp 21 | 22 | from . import sympy, protocol, wrapper 23 | 24 | 25 | __all__ = ["Expression", "ExpressionVector", "ExpressionScalar", 26 | "NonNumericEvaluation", "ExpressionVariableMissingException"] 27 | 28 | 29 | Expression: Type[protocol.Expression] = sympy.Expression 30 | ExpressionScalar: Type[protocol.ExpressionScalar] = sympy.ExpressionScalar 31 | ExpressionVector: Type[protocol.ExpressionVector] = sympy.ExpressionVector 32 | 33 | 34 | if os.environ.get('QUPULSE_EXPRESSION_WRAPPER', None): # pragma: no cover 35 | Expression, ExpressionScalar, ExpressionVector = wrapper.make_wrappers(sympy.Expression, 36 | sympy.ExpressionScalar, 37 | sympy.ExpressionVector) 38 | 39 | 40 | ExpressionLike = TypeVar('ExpressionLike', str, Real, sp.Expr, ExpressionScalar) 41 | 42 | 43 | class ExpressionVariableMissingException(Exception): 44 | """An exception indicating that a variable value was not provided during expression evaluation. 45 | 46 | See also: 47 | qupulse.expressions.Expression 48 | """ 49 | 50 | def __init__(self, variable: str, expression: Expression) -> None: 51 | super().__init__() 52 | self.variable = variable 53 | self.expression = expression 54 | 55 | def __str__(self) -> str: 56 | return f"Could not evaluate <{self.expression}>: A value for variable <{self.variable}> is missing!" 57 | 58 | 59 | class NonNumericEvaluation(Exception): 60 | """An exception that is raised if the result of evaluate_numeric is not a number. 61 | 62 | See also: 63 | qupulse.expressions.Expression.evaluate_numeric 64 | """ 65 | 66 | def __init__(self, expression: Expression, non_numeric_result, call_arguments): 67 | self.expression = expression 68 | self.non_numeric_result = non_numeric_result 69 | self.call_arguments = call_arguments 70 | 71 | def __str__(self) -> str: 72 | if isinstance(self.non_numeric_result, np.ndarray): 73 | dtype = self.non_numeric_result.dtype 74 | 75 | if dtype == np.dtype('O'): 76 | dtypes = set(map(type, self.non_numeric_result.flat)) 77 | return f"The result of evaluate_numeric is an array with the types {dtypes} which is not purely numeric" 78 | else: 79 | dtype = type(self.non_numeric_result) 80 | return f"The result of evaluate_numeric is of type {dtype} which is not a number" 81 | -------------------------------------------------------------------------------- /qupulse/expressions/protocol.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | """This module contains the interface / protocol descriptions of ``Expression``, ``ExpressionScalar`` and 6 | ``ExpressionVector``.""" 7 | 8 | from typing import Mapping, Union, Sequence, Hashable, Any, Protocol 9 | from numbers import Real 10 | 11 | import numpy as np 12 | 13 | 14 | class Ordered(Protocol): 15 | def __lt__(self, other): 16 | pass 17 | 18 | def __le__(self, other): 19 | pass 20 | 21 | def __gt__(self, other): 22 | pass 23 | 24 | def __ge__(self, other): 25 | pass 26 | 27 | 28 | class Scalar(Protocol): 29 | def __add__(self, other): 30 | pass 31 | 32 | def __sub__(self, other): 33 | pass 34 | 35 | def __mul__(self, other): 36 | pass 37 | 38 | def __truediv__(self, other): 39 | pass 40 | 41 | def __floordiv__(self, other): 42 | pass 43 | 44 | def __ceil__(self): 45 | pass 46 | 47 | def __floor__(self): 48 | pass 49 | 50 | def __float__(self): 51 | pass 52 | 53 | def __int__(self): 54 | pass 55 | 56 | def __abs__(self): 57 | pass 58 | 59 | 60 | class Expression(Hashable, Protocol): 61 | """This protocol defines how Expressions are allowed to be used in qupulse.""" 62 | 63 | def evaluate_in_scope(self, scope: Mapping) -> Union[Real, np.ndarray]: 64 | """Evaluate the expression by taking the variables from the given scope (typically of type Scope, but it can be 65 | any mapping.) 66 | Args: 67 | scope: 68 | 69 | Returns: 70 | 71 | """ 72 | 73 | def evaluate_symbolic(self, substitutions: Mapping[str, Any]) -> 'Expression': 74 | """Substitute a part of the expression for another""" 75 | 76 | def evaluate_time_dependent(self, scope: Mapping) -> Union['Expression', Real, np.ndarray]: 77 | """Evaluate to a time dependent expression or a constant.""" 78 | 79 | @property 80 | def variables(self) -> Sequence[str]: 81 | """ Get all free variables in the expression. 82 | 83 | Returns: 84 | A collection of all free variables occurring in the expression. 85 | """ 86 | raise NotImplementedError() 87 | 88 | @classmethod 89 | def make(cls, 90 | expression_or_dict, 91 | numpy_evaluation=None) -> 'Expression': 92 | """Backward compatible expression generation to allow creation from dict.""" 93 | raise NotImplementedError() 94 | 95 | @property 96 | def underlying_expression(self) -> Any: 97 | """Return some internal unspecified representation""" 98 | raise NotImplementedError() 99 | 100 | def get_serialization_data(self): 101 | raise NotImplementedError() 102 | 103 | 104 | class ExpressionScalar(Expression, Scalar, Ordered, Protocol): 105 | pass 106 | 107 | 108 | class ExpressionVector(Expression, Protocol): 109 | pass 110 | -------------------------------------------------------------------------------- /qupulse/expressions/wrapper.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | """This module contains the function :py:``make_wrappers`` to define wrapper classes for expression protocol implementations 6 | which only implements methods of the protocol. 7 | It is used for finding code that relies on expression implementation details.""" 8 | 9 | import math 10 | from typing import Sequence, Any, Mapping, Union, Tuple 11 | from numbers import Real 12 | 13 | import numpy as np 14 | 15 | from qupulse.expressions import protocol, sympy 16 | 17 | 18 | def make_wrappers(expr: type, expr_scalar: type, expr_vector: type) -> Tuple[type, type, type]: 19 | """Create wrappers for expression base, scalar and vector types that only expose the methods defined in the 20 | corresponding expression protocol classes. 21 | 22 | The vector is currently not implemented. 23 | 24 | Args: 25 | expr: Expression base type of the implementation 26 | expr_scalar: Expression scalar type of the implementation 27 | expr_vector: Expression vector type of the implementation 28 | 29 | Returns: 30 | A tuple of (base, scalar, vector) types that wrap the given types. 31 | """ 32 | 33 | class ExpressionWrapper(protocol.Expression): 34 | def __init__(self, x): 35 | self._wrapped: protocol.Expression = expr(x) 36 | 37 | @classmethod 38 | def make(cls, expression_or_dict, numpy_evaluation=None) -> 'ExpressionWrapper': 39 | return cls(expression_or_dict) 40 | 41 | @property 42 | def underlying_expression(self) -> Any: 43 | return self._wrapped.underlying_expression 44 | 45 | def __hash__(self) -> int: 46 | return hash(self._wrapped) 47 | 48 | def __eq__(self, other): 49 | return self._wrapped == getattr(other, '_wrapped', other) 50 | 51 | @property 52 | def variables(self) -> Sequence[str]: 53 | return self._wrapped.variables 54 | 55 | def evaluate_in_scope(self, scope: Mapping) -> Union[Real, np.ndarray]: 56 | return self._wrapped.evaluate_in_scope(scope) 57 | 58 | def evaluate_symbolic(self, substitutions: Mapping[str, Any]) -> 'ExpressionWrapper': 59 | """Substitute a part of the expression for another""" 60 | return ExpressionWrapper(self._wrapped.evaluate_symbolic(substitutions)) 61 | 62 | def evaluate_time_dependent(self, scope: Mapping) -> Union['Expression', Real, np.ndarray]: 63 | """Evaluate to a time dependent expression or a constant.""" 64 | return self._wrapped.evaluate_time_dependent(scope) 65 | 66 | def get_serialization_data(self): 67 | return self._wrapped.get_serialization_data() 68 | 69 | class ExpressionScalarWrapper(ExpressionWrapper, protocol.ExpressionScalar): 70 | def __init__(self, x): 71 | ExpressionWrapper.__init__(self, 0) 72 | self._wrapped: protocol.ExpressionScalar = expr_scalar(x) 73 | 74 | # Scalar 75 | def __add__(self, other): 76 | return ExpressionScalarWrapper(self._wrapped + getattr(other, '_wrapped', other)) 77 | 78 | def __sub__(self, other): 79 | return ExpressionScalarWrapper(self._wrapped - getattr(other, '_wrapped', other)) 80 | 81 | def __mul__(self, other): 82 | return ExpressionScalarWrapper(self._wrapped * getattr(other, '_wrapped', other)) 83 | 84 | def __truediv__(self, other): 85 | return ExpressionScalarWrapper(self._wrapped / getattr(other, '_wrapped', other)) 86 | 87 | def __floordiv__(self, other): 88 | return ExpressionScalarWrapper(self._wrapped // getattr(other, '_wrapped', other)) 89 | 90 | def __ceil__(self): 91 | return ExpressionScalarWrapper(math.ceil(self._wrapped)) 92 | 93 | def __floor__(self): 94 | return ExpressionScalarWrapper(math.floor(self._wrapped)) 95 | 96 | def __float__(self): 97 | return float(self._wrapped) 98 | 99 | def __int__(self): 100 | return int(self._wrapped) 101 | 102 | def __abs__(self): 103 | return ExpressionScalarWrapper(abs(self._wrapped)) 104 | 105 | # Ordered 106 | def __lt__(self, other): 107 | return self._wrapped < getattr(other, '_wrapped', other) 108 | 109 | def __le__(self, other): 110 | return self._wrapped <= getattr(other, '_wrapped', other) 111 | 112 | def __gt__(self, other): 113 | return self._wrapped > getattr(other, '_wrapped', other) 114 | 115 | def __ge__(self, other): 116 | return self._wrapped >= getattr(other, '_wrapped', other) 117 | 118 | class ExpressionVectorWrapper(ExpressionWrapper): 119 | pass 120 | 121 | return ExpressionWrapper, ExpressionScalarWrapper, ExpressionVectorWrapper 122 | -------------------------------------------------------------------------------- /qupulse/hardware/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | """Contains drivers for AWG control and digitizer configuration as well as a unifying interface to all instruments: 6 | :class:`~qupulse.hardware.setup.HardwareSetup`""" 7 | 8 | from qupulse.hardware.setup import HardwareSetup 9 | 10 | from qupulse.hardware import awgs 11 | from qupulse.hardware import dacs 12 | 13 | __all__ = ["HardwareSetup", "awgs", "dacs"] 14 | -------------------------------------------------------------------------------- /qupulse/hardware/awgs/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | import lazy_loader as lazy 6 | 7 | 8 | __getattr__, __dir__, __all__ = lazy.attach( 9 | __name__, 10 | submodules={'base'}, 11 | submod_attrs={ 12 | 'tabor': ['TaborAWGRepresentation', 'TaborChannelPair'], 13 | 'tektronix': ['TektronixAWG'], 14 | 'zihdawg': ['HDAWGRepresentation', 'HDAWGChannelGroup'], 15 | } 16 | ) 17 | -------------------------------------------------------------------------------- /qupulse/hardware/awgs/dummy.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from typing import Tuple, Set 6 | 7 | from .base import AWG, ProgramOverwriteException 8 | 9 | class DummyAWG(AWG): 10 | def __init__(self, 11 | sample_rate: float=10, 12 | output_range: Tuple[float, float]=(-5, 5), 13 | num_channels: int=1, 14 | num_markers: int=1) -> None: 15 | """Dummy AWG for automated testing, debugging and usage in examples. 16 | 17 | Args: 18 | sample_rate (float): The sample rate of the dummy. (default = 10) 19 | output_range (float, float): A (min,max)-tuple of possible output values. 20 | (default = (-5,5)). 21 | """ 22 | super().__init__(identifier="DummyAWG{0}".format(id(self))) 23 | 24 | self._programs = {} # contains program names and programs 25 | self._sample_rate = sample_rate 26 | self._output_range = output_range 27 | self._num_channels = num_channels 28 | self._num_markers = num_markers 29 | self._channels = ('default',) 30 | self._armed = None 31 | 32 | def set_volatile_parameters(self, program_name: str, parameters): 33 | raise NotImplementedError() 34 | 35 | def upload(self, name, program, channels, markers, voltage_transformation, force=False) -> None: 36 | if name in self.programs: 37 | if not force: 38 | raise ProgramOverwriteException(name) 39 | else: 40 | self.remove(name) 41 | self.upload(name, program, channels, markers, voltage_transformation) 42 | else: 43 | self._programs[name] = (program, channels, markers, voltage_transformation) 44 | 45 | def remove(self, name) -> None: 46 | if name in self.programs: 47 | self._programs.pop(name) 48 | 49 | def clear(self) -> None: 50 | self._programs = {} 51 | 52 | def arm(self, name: str) -> None: 53 | self._armed = name 54 | 55 | @property 56 | def programs(self) -> Set[str]: 57 | return set(self._programs.keys()) 58 | 59 | @property 60 | def output_range(self) -> Tuple[float, float]: 61 | return self._output_range 62 | 63 | @property 64 | def identifier(self) -> str: 65 | return "DummyAWG{0}".format(id(self)) 66 | 67 | @property 68 | def sample_rate(self) -> float: 69 | return self._sample_rate 70 | 71 | @property 72 | def num_channels(self): 73 | return self._num_channels 74 | 75 | @property 76 | def num_markers(self): 77 | return self._num_markers 78 | -------------------------------------------------------------------------------- /qupulse/hardware/awgs/zihdawg.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | import sys 6 | import argparse 7 | import logging 8 | 9 | from typing import Set 10 | 11 | if sys.version_info.minor > 8: 12 | try: 13 | from qupulse_hdawg.zihdawg import * 14 | except ImportError: 15 | print("Install the qupulse_hdawg package to use HDAWG with this python version.") 16 | raise 17 | else: 18 | try: 19 | from qupulse_hdawg_legacy.zihdawg import * 20 | except ImportError: 21 | print("Install the qupulse_hdawg_legacy package to use HDAWG with this python version.") 22 | raise 23 | 24 | 25 | def example_upload(hdawg_kwargs: dict, channels: Set[int], markers: Set[Tuple[int, int]]): # pragma: no cover 26 | from qupulse.pulses import TablePT, SequencePT, RepetitionPT 27 | if isinstance(hdawg_kwargs, dict): 28 | hdawg = HDAWGRepresentation(**hdawg_kwargs) 29 | else: 30 | hdawg = hdawg_kwargs 31 | 32 | assert not set(channels) - set(range(8)), "Channels must be in 0..=7" 33 | channels = sorted(channels) 34 | 35 | required_channels = {*channels, *(ch for ch, _ in markers)} 36 | channel_group = get_group_for_channels(hdawg, required_channels) 37 | channel_group_channels = range(channel_group.awg_group_index * channel_group.num_channels, 38 | (channel_group.awg_group_index + 1) * channel_group.num_channels) 39 | 40 | # choose length based on minimal sample rate 41 | sample_rate = channel_group.sample_rate / 10**9 42 | min_t = channel_group.MIN_WAVEFORM_LEN / sample_rate 43 | quant_t = channel_group.WAVEFORM_LEN_QUANTUM / sample_rate 44 | 45 | assert min_t > 4 * quant_t, "Example not updated" 46 | 47 | entry_list1 = [(0, 0), (quant_t * 2, .2, 'hold'), (min_t, .3, 'linear'), (min_t + 3*quant_t, 0, 'jump')] 48 | entry_list2 = [(0, 0), (quant_t * 3, -.2, 'hold'), (min_t, -.3, 'linear'), (min_t + 4*quant_t, 0, 'jump')] 49 | entry_list3 = [(0, 0), (quant_t * 1, -.2, 'linear'), (min_t, -.3, 'linear'), (2*min_t, 0, 'jump')] 50 | entry_lists = [entry_list1, entry_list2, entry_list3] 51 | 52 | entry_dict1 = {ch: entry_lists[:2][i % 2] for i, ch in enumerate(channels)} 53 | entry_dict2 = {ch: entry_lists[1::-1][i % 2] for i, ch in enumerate(channels)} 54 | entry_dict3 = {ch: entry_lists[2:0:-1][i % 2] for i, ch in enumerate(channels)} 55 | 56 | tpt1 = TablePT(entry_dict1, measurements=[('m', 20, 30)]) 57 | tpt2 = TablePT(entry_dict2) 58 | tpt3 = TablePT(entry_dict3, measurements=[('m', 10, 50)]) 59 | rpt = RepetitionPT(tpt1, 4) 60 | spt = SequencePT(tpt2, rpt) 61 | rpt2 = RepetitionPT(spt, 2) 62 | spt2 = SequencePT(rpt2, tpt3) 63 | p = spt2.create_program() 64 | 65 | upload_ch = tuple(ch if ch in channels else None 66 | for ch in channel_group_channels) 67 | upload_mk = (None,) * channel_group.num_markers 68 | upload_vt = (lambda x: x,) * channel_group.num_channels 69 | 70 | channel_group.upload('pulse_test1', p, upload_ch, upload_mk, upload_vt) 71 | 72 | if markers: 73 | markers = sorted(markers) 74 | assert len(markers) == len(set(markers)) 75 | channel_group_markers = tuple((ch, mk) 76 | for ch in channel_group_channels 77 | for mk in (0, 1)) 78 | 79 | full_on = [(0, 1), (min_t, 1)] 80 | two_3rd = [(0, 1), (min_t*2/3, 0), (min_t, 0)] 81 | one_3rd = [(0, 0), (min_t*2/3, 1), (min_t, 1)] 82 | 83 | marker_start = TablePT({'m0': full_on, 'm1': full_on}) 84 | marker_body = TablePT({'m0': two_3rd, 'm1': one_3rd}) 85 | 86 | marker_test_pulse = marker_start @ RepetitionPT(marker_body, 10000) 87 | 88 | marker_program = marker_test_pulse.create_program() 89 | 90 | upload_ch = (None, ) * channel_group.num_channels 91 | upload_mk = tuple(f"m{mk}" if (ch, mk) in markers else None 92 | for (ch, mk) in channel_group_markers) 93 | 94 | channel_group.upload('marker_test', marker_program, upload_ch, upload_mk, upload_vt) 95 | 96 | try: 97 | while True: 98 | for program in channel_group.programs: 99 | print(f'playing {program}') 100 | channel_group.arm(program) 101 | channel_group.run_current_program() 102 | while not channel_group.was_current_program_finished(): 103 | print(f'waiting for {program} to finish') 104 | time.sleep(1e-2) 105 | finally: 106 | channel_group.enable(False) 107 | 108 | 109 | if __name__ == "__main__": 110 | import sys 111 | args = argparse.ArgumentParser('Upload an example pulse to a HDAWG') 112 | args.add_argument('device_serial', help='device serial of the form dev1234') 113 | args.add_argument('device_interface', help='device interface', choices=['USB', '1GbE'], default='1GbE', nargs='?') 114 | args.add_argument('--channels', help='channels to use', choices=range(8), default=[0, 1], type=int, nargs='+') 115 | args.add_argument('--markers', help='markers to use', choices=range(8*2), default=[], type=int, nargs='*') 116 | parsed = vars(args.parse_args()) 117 | 118 | channels = parsed.pop('channels') 119 | markers = [(m // 2, m % 2) for m in parsed.pop('markers')] 120 | 121 | logging.basicConfig(stream=sys.stdout) 122 | example_upload(hdawg_kwargs=parsed, channels=channels, markers=markers) 123 | -------------------------------------------------------------------------------- /qupulse/hardware/dacs/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | import lazy_loader as lazy 6 | 7 | __getattr__, __dir__, __all__ = lazy.attach( 8 | __name__, 9 | submodules={'alazar2'}, 10 | submod_attrs={ 11 | 'dac_base': ['DAC'], 12 | 'alazar': ['AlazarCard'], 13 | } 14 | ) 15 | -------------------------------------------------------------------------------- /qupulse/hardware/dacs/dac_base.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from abc import ABCMeta, abstractmethod 6 | from typing import Dict, Tuple, Iterable, TYPE_CHECKING 7 | 8 | if TYPE_CHECKING: 9 | import numpy 10 | else: 11 | numpy = None 12 | 13 | __all__ = ['DAC'] 14 | 15 | 16 | class DAC(metaclass=ABCMeta): 17 | """Representation of a data acquisition card""" 18 | 19 | @abstractmethod 20 | def register_measurement_windows(self, program_name: str, windows: Dict[str, Tuple['numpy.ndarray', 21 | 'numpy.ndarray']]) -> None: 22 | """Register measurement windows for a given program. Overwrites previously defined measurement windows for 23 | this program. 24 | 25 | Args: 26 | program_name: Name of the program 27 | windows: Measurement windows by name. 28 | First array are the start points of measurement windows in nanoseconds. 29 | Second array are the corresponding measurement window's lengths in nanoseconds. 30 | """ 31 | 32 | @abstractmethod 33 | def set_measurement_mask(self, program_name: str, mask_name: str, 34 | begins: 'numpy.ndarray', 35 | lengths: 'numpy.ndarray') -> Tuple['numpy.ndarray', 'numpy.ndarray']: 36 | """Set/overwrite a single the measurement mask for a program. Begins and lengths are in nanoseconds. 37 | 38 | Args: 39 | program_name: Name of the program 40 | mask_name: Name of the mask/measurement windows 41 | begins: Staring points in nanoseconds 42 | lengths: Lengths in nanoseconds 43 | 44 | Returns: 45 | Measurement windows in DAC samples (begins, lengths) 46 | """ 47 | 48 | @abstractmethod 49 | def register_operations(self, program_name: str, operations) -> None: 50 | """Register operations that are to be applied to the measurement results. 51 | 52 | Args: 53 | program_name: Name of the program 54 | operations: DAC specific instructions what to do with the data recorded by the device. 55 | """ 56 | 57 | @abstractmethod 58 | def arm_program(self, program_name: str) -> None: 59 | """Prepare the device for measuring the given program and wait for a trigger event.""" 60 | 61 | @abstractmethod 62 | def delete_program(self, program_name) -> None: 63 | """Delete program from internal memory.""" 64 | 65 | @abstractmethod 66 | def clear(self) -> None: 67 | """Clears all registered programs.""" 68 | 69 | @abstractmethod 70 | def measure_program(self, channels: Iterable[str]) -> Dict[str, 'numpy.ndarray']: 71 | """Get the last measurement's results of the specified operations/channels""" 72 | -------------------------------------------------------------------------------- /qupulse/hardware/dacs/dummy.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from typing import Tuple, Set, Dict 6 | from collections import deque 7 | 8 | from qupulse.hardware.dacs.dac_base import DAC 9 | 10 | class DummyDAC(DAC): 11 | """Dummy DAC for automated testing, debugging and usage in examples. """ 12 | 13 | def __init__(self): 14 | self._measurement_windows = dict() 15 | self._operations = dict() 16 | self.measured_data = deque([]) 17 | self._meas_masks = {} 18 | self._armed_program = None 19 | 20 | @property 21 | def armed_program(self): 22 | return self._armed_program 23 | 24 | def register_measurement_windows(self, program_name: str, windows: Dict[str, Tuple['numpy.ndarray', 25 | 'numpy.ndarray']]): 26 | self._measurement_windows[program_name] = windows 27 | 28 | def register_operations(self, program_name: str, operations): 29 | self._operations[program_name] = operations 30 | 31 | def arm_program(self, program_name: str): 32 | self._armed_program = program_name 33 | 34 | def delete_program(self, program_name): 35 | if program_name in self._operations: 36 | self._operations.pop(program_name) 37 | if program_name in self._measurement_windows: 38 | self._measurement_windows.pop(program_name) 39 | 40 | def clear(self) -> None: 41 | self._measurement_windows = dict() 42 | self._operations = dict() 43 | self._armed_program = None 44 | 45 | def measure_program(self, channels): 46 | return self.measured_data.pop() 47 | 48 | def set_measurement_mask(self, program_name, mask_name, begins, lengths) -> Tuple['numpy.ndarray', 'numpy.ndarray']: 49 | self._meas_masks.setdefault(program_name, {})[mask_name] = (begins, lengths) 50 | return begins, lengths 51 | -------------------------------------------------------------------------------- /qupulse/hardware/feature_awg/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qutech/qupulse/15d67075362c3a056b942f8feb0818f48a5cc1cb/qupulse/hardware/feature_awg/__init__.py -------------------------------------------------------------------------------- /qupulse/hardware/feature_awg/base_features.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from types import MappingProxyType 6 | from typing import Callable, Generic, Mapping, Optional, Type, TypeVar 7 | from abc import ABC 8 | 9 | __all__ = ["Feature", "FeatureAble"] 10 | 11 | 12 | class Feature: 13 | """ 14 | Base class for features of :class:`.FeatureAble`. 15 | """ 16 | def __init__(self, target_type: Type["FeatureAble"]): 17 | self._target_type = target_type 18 | 19 | @property 20 | def target_type(self) -> Type["FeatureAble"]: 21 | return self._target_type 22 | 23 | 24 | FeatureType = TypeVar("FeatureType", bound=Feature) 25 | GetItemFeatureType = TypeVar("GetItemFeatureType", bound=Feature) 26 | 27 | 28 | class FeatureAble(Generic[FeatureType]): 29 | """ 30 | Base class for all types that are able to handle features. The features are saved in a dictionary and the methods 31 | can be called with the `__getitem__`-operator. 32 | """ 33 | 34 | def __init__(self): 35 | super().__init__() 36 | self._features = {} 37 | 38 | def __getitem__(self, feature_type: Type[GetItemFeatureType]) -> GetItemFeatureType: 39 | if isinstance(feature_type, str): 40 | return self._features[feature_type] 41 | if not isinstance(feature_type, type): 42 | raise TypeError("Expected type-object as key, got \"{ftt}\" instead".format( 43 | ftt=type(feature_type).__name__)) 44 | key_type = _get_base_feature_type(feature_type) 45 | if key_type is None: 46 | raise TypeError("Unexpected type of feature: {ft}".format(ft=feature_type.__name__)) 47 | if key_type not in self._features: 48 | raise KeyError("Could not get feature for type: {ft}".format(ft=feature_type.__name__)) 49 | return self._features[key_type] 50 | 51 | def add_feature(self, feature: FeatureType) -> None: 52 | """ 53 | The method adds the feature to a Dictionary with all features 54 | 55 | Args: 56 | feature: A certain feature which functions should be added to the dictionary _features 57 | """ 58 | feature_type = _get_base_feature_type(type(feature)) 59 | if feature_type is None: 60 | raise TypeError("Unexpected type of feature: {ft}".format(ft=type(feature).__name__)) 61 | if not isinstance(self, feature.target_type): 62 | raise TypeError("Features with type \"{ft}\" belong to \"{tt}\"-objects".format( 63 | ft=type(feature).__name__, tt=feature.target_type.__name__)) 64 | if feature_type in self._features: 65 | raise KeyError(self, "Feature with type \"{ft}\" already exists".format(ft=feature_type.__name__)) 66 | self._features[feature_type] = feature 67 | # Also adding the feature with the string as the key. With this you can you the name as a string for __getitem__ 68 | self._features[feature_type.__name__] = feature 69 | 70 | @property 71 | def features(self) -> Mapping[FeatureType, Callable]: 72 | """Returns the dictionary with all features of a FeatureAble""" 73 | return MappingProxyType(self._features) 74 | 75 | 76 | def _get_base_feature_type(feature_type: Type[Feature]) -> Type[Optional[Feature]]: 77 | """ 78 | This function searches for the second inheritance level under `Feature` (i.e. level under `AWGDeviceFeature`, 79 | `AWGChannelFeature` or `AWGChannelTupleFeature`). This is done to ensure, that nobody adds the same feature 80 | twice, but with a type of a different inheritance level as key. 81 | 82 | Args: 83 | feature_type: Type of the feature 84 | 85 | Returns: 86 | Base type of the feature_type, two inheritance levels under `Feature` 87 | """ 88 | if not issubclass(feature_type, Feature): 89 | return type(None) 90 | 91 | # Search for base class on the inheritance line of Feature 92 | for base in feature_type.__bases__: 93 | if issubclass(base, Feature): 94 | result_type = base 95 | break 96 | else: 97 | return type(None) 98 | 99 | if Feature in result_type.__bases__: 100 | return feature_type 101 | else: 102 | return _get_base_feature_type(result_type) 103 | -------------------------------------------------------------------------------- /qupulse/hardware/feature_awg/channel_tuple_wrapper.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from typing import Tuple, Optional, Callable, Set 6 | 7 | from qupulse import ChannelID 8 | from qupulse.program.loop import Loop 9 | from qupulse.hardware.feature_awg.base import AWGChannelTuple 10 | from qupulse.hardware.feature_awg.features import ProgramManagement, VolatileParameters 11 | from qupulse.hardware.awgs.base import AWG 12 | 13 | 14 | class ChannelTupleAdapter(AWG): 15 | """ 16 | This class serves as an adapter between the old Class AWG and the new driver abstraction. It routes all the methods 17 | the AWG class to the corresponding methods of the new driver. 18 | """ 19 | def __copy__(self) -> None: 20 | pass 21 | 22 | def __init__(self, channel_tuple: AWGChannelTuple): 23 | super().__init__(channel_tuple.name) 24 | self._channel_tuple = channel_tuple 25 | 26 | @property 27 | def num_channels(self) -> int: 28 | return len(self._channel_tuple.channels) 29 | 30 | @property 31 | def num_markers(self) -> int: 32 | return len(self._channel_tuple.marker_channels) 33 | 34 | def upload(self, name: str, 35 | program: Loop, 36 | channels: Tuple[Optional[ChannelID], ...], 37 | markers: Tuple[Optional[ChannelID], ...], 38 | voltage_transformation: Tuple[Optional[Callable], ...], 39 | force: bool = False) -> None: 40 | return self._channel_tuple[ProgramManagement].upload(name=name, program=program, 41 | channels=channels, 42 | marker_channels=markers, 43 | voltage_transformation=voltage_transformation, 44 | repetition_mode=None, 45 | force=force) 46 | 47 | def remove(self, name: str) -> None: 48 | return self._channel_tuple[ProgramManagement].remove(name) 49 | 50 | def clear(self) -> None: 51 | return self._channel_tuple[ProgramManagement].clear() 52 | 53 | def arm(self, name: Optional[str]) -> None: 54 | return self._channel_tuple[ProgramManagement].arm(name) 55 | 56 | @property 57 | def programs(self) -> Set[str]: 58 | return self._channel_tuple[ProgramManagement].programs 59 | 60 | @property 61 | def sample_rate(self) -> float: 62 | return self._channel_tuple.sample_rate 63 | 64 | def set_volatile_parameters(self, program_name: str, parameters): 65 | self._channel_tuple[VolatileParameters].set_volatile_parameters(program_name, parameters) 66 | 67 | -------------------------------------------------------------------------------- /qupulse/program/volatile.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from typing import NamedTuple, Mapping 6 | import warnings 7 | import numbers 8 | 9 | 10 | from qupulse.parameter_scope import Scope, MappedScope, JointScope 11 | from qupulse.expressions import Expression, ExpressionScalar 12 | from qupulse.utils.types import FrozenDict, FrozenMapping 13 | from qupulse.utils import is_integer 14 | 15 | 16 | __all__ = ['VolatileProperty', 'VolatileValue', 'VolatileRepetitionCount'] 17 | 18 | 19 | VolatileProperty = NamedTuple('VolatileProperty', [('expression', Expression), 20 | ('dependencies', FrozenMapping[str, Expression])]) 21 | VolatileProperty.__doc__ = """Hashable representation of a volatile program property. It does not contain the concrete 22 | value. Using the dependencies attribute to calculate the value might yield unexpected results.""" 23 | 24 | 25 | class VolatileValue: 26 | """Not hashable""" 27 | 28 | def __init__(self, expression: ExpressionScalar, scope: Scope): 29 | self._expression = expression 30 | self._scope = scope 31 | 32 | @property 33 | def volatile_property(self) -> VolatileProperty: 34 | dependencies = self._scope.get_volatile_parameters() 35 | dependencies = FrozenDict({parameter_name: dependencies[parameter_name] 36 | for parameter_name in self._expression.variables 37 | if parameter_name in dependencies}) 38 | return VolatileProperty(expression=self._expression, dependencies=dependencies) 39 | 40 | @classmethod 41 | def operation(cls, expression, **operands): 42 | expression = Expression(expression) 43 | assert set(expression.variables) == operands.keys() 44 | 45 | scope = JointScope(FrozenDict( 46 | {operand_name: MappedScope(operand._scope, FrozenDict({operand_name: operand._expression})) 47 | for operand_name, operand in operands.items()} 48 | )) 49 | return cls(expression, scope) 50 | 51 | def __sub__(self, other: int): 52 | return type(self)(self._expression - other, self._scope) 53 | 54 | def __mul__(self, other: int): 55 | return type(self)(self._expression * other, self._scope) 56 | 57 | 58 | class VolatileRepetitionCount(VolatileValue): 59 | def __int__(self): 60 | value = self._expression.evaluate_in_scope(self._scope) 61 | if not is_integer(value): 62 | warnings.warn("Repetition count is no integer. Rounding might lead to unexpected results.") 63 | value = int(round(value)) 64 | if value < 0: 65 | warnings.warn("Repetition count is negative. Clamping lead to unexpected results.") 66 | value = 0 67 | return value 68 | 69 | def update_volatile_dependencies(self, new_constants: Mapping[str, numbers.Number]) -> int: 70 | self._scope = self._scope.change_constants(new_constants) 71 | return int(self) 72 | 73 | def __eq__(self, other): 74 | if type(self) == type(other): 75 | return self._scope is other._scope and self._expression == other._expression 76 | else: 77 | return NotImplemented 78 | -------------------------------------------------------------------------------- /qupulse/pulses/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | """This is the central package for defining pulses. All :class:`~qupulse.pulses.pulse_template.PulseTemplate` 6 | subclasses that are final and ready to be used are imported here with their recommended abbreviation as an alias. 7 | 8 | See :class:`.PulseTemplate`""" 9 | 10 | from qupulse.pulses.abstract_pulse_template import AbstractPulseTemplate as AbstractPT 11 | from qupulse.pulses.function_pulse_template import FunctionPulseTemplate as FunctionPT 12 | from qupulse.pulses.loop_pulse_template import ForLoopPulseTemplate as ForLoopPT 13 | from qupulse.pulses.multi_channel_pulse_template import AtomicMultiChannelPulseTemplate as AtomicMultiChannelPT,\ 14 | ParallelConstantChannelPulseTemplate as ParallelConstantChannelPT,\ 15 | ParallelChannelPulseTemplate as ParallelChannelPT 16 | from qupulse.pulses.mapping_pulse_template import MappingPulseTemplate as MappingPT 17 | from qupulse.pulses.repetition_pulse_template import RepetitionPulseTemplate as RepetitionPT 18 | from qupulse.pulses.sequence_pulse_template import SequencePulseTemplate as SequencePT 19 | from qupulse.pulses.table_pulse_template import TablePulseTemplate as TablePT 20 | from qupulse.pulses.constant_pulse_template import ConstantPulseTemplate as ConstantPT 21 | from qupulse.pulses.point_pulse_template import PointPulseTemplate as PointPT 22 | from qupulse.pulses.arithmetic_pulse_template import ArithmeticPulseTemplate as ArithmeticPT,\ 23 | ArithmeticAtomicPulseTemplate as ArithmeticAtomicPT 24 | from qupulse.pulses.time_reversal_pulse_template import TimeReversalPulseTemplate as TimeReversalPT 25 | 26 | import warnings 27 | with warnings.catch_warnings(): 28 | warnings.simplefilter('ignore') 29 | # ensure this is included.. it adds a deserialization handler for pulse_template_parameter_mapping.MappingPT 30 | # which is not present otherwise 31 | import qupulse.pulses.pulse_template_parameter_mapping 32 | del qupulse 33 | del warnings 34 | 35 | 36 | __all__ = ["FunctionPT", "ForLoopPT", "AtomicMultiChannelPT", "MappingPT", "RepetitionPT", "SequencePT", "TablePT", 37 | "PointPT", "ConstantPT", "AbstractPT", "ParallelConstantChannelPT", "ArithmeticPT", "ArithmeticAtomicPT", 38 | "TimeReversalPT", "ParallelChannelPT"] 39 | -------------------------------------------------------------------------------- /qupulse/pulses/measurement.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from typing import Optional, List, Tuple, Union, Dict, Set, Mapping, AbstractSet 6 | from numbers import Real 7 | import itertools 8 | 9 | from qupulse.expressions import Expression, ExpressionScalar 10 | from qupulse.utils.types import MeasurementWindow 11 | from qupulse.parameter_scope import Scope 12 | 13 | MeasurementDeclaration = Tuple[str, Union[Expression, str, Real], Union[Expression, str, Real]] 14 | 15 | 16 | class MeasurementDefiner: 17 | def __init__(self, measurements: Optional[List[MeasurementDeclaration]]): 18 | if measurements is None: 19 | self._measurement_windows = [] 20 | else: 21 | self._measurement_windows = [(name, 22 | begin if isinstance(begin, Expression) else ExpressionScalar(begin), 23 | length if isinstance(length, Expression) else ExpressionScalar(length)) 24 | for name, begin, length in measurements] 25 | for _, _, length in self._measurement_windows: 26 | if (length < 0) is True: 27 | raise ValueError('Measurement window length may not be negative') 28 | 29 | def get_measurement_windows(self, 30 | parameters: Union[Mapping[str, Real], Scope], 31 | measurement_mapping: Dict[str, Optional[str]]) -> List[MeasurementWindow]: 32 | """Calculate measurement windows with the given parameter set and rename them with the measurement mapping. This 33 | method only returns the measurement windows that are defined on `self`. It does _not_ collect the measurement 34 | windows defined on eventual child objects that `self` has/is composed of. 35 | 36 | Args: 37 | parameters: Used to calculate the numeric begins and lengths of symbolically defined measurement windows. 38 | measurement_mapping: Used to rename/drop measurement windows. Windows mapped to None are dropped. 39 | 40 | Returns: 41 | List of measurement windows directly defined on self 42 | """ 43 | try: 44 | volatile = parameters.get_volatile_parameters().keys() 45 | except AttributeError: 46 | volatile = frozenset() 47 | 48 | resulting_windows = [] 49 | for name, begin, length in self._measurement_windows: 50 | name = measurement_mapping[name] 51 | if name is None: 52 | continue 53 | 54 | assert volatile.isdisjoint(begin.variables) and volatile.isdisjoint(length.variables), "volatile measurement parameters are not supported" 55 | 56 | begin_val = begin.evaluate_in_scope(parameters) 57 | length_val = length.evaluate_in_scope(parameters) 58 | if begin_val < 0 or length_val < 0: 59 | raise ValueError('Measurement window with negative begin or length: {}, {}'.format(begin, length)) 60 | 61 | resulting_windows.append( 62 | (name, 63 | begin_val, 64 | length_val) 65 | ) 66 | return resulting_windows 67 | 68 | @property 69 | def measurement_parameters(self) -> AbstractSet[str]: 70 | """Return the parameters of measurements that are directly declared on `self`. 71 | Does _not_ visit eventual child objects.""" 72 | return set(var 73 | for _, begin, length in self._measurement_windows 74 | for var in itertools.chain(begin.variables, length.variables)) 75 | 76 | @property 77 | def measurement_declarations(self) -> List[MeasurementDeclaration]: 78 | """Return the measurements that are directly declared on `self`. Does _not_ visit eventual child objects.""" 79 | return [(name, 80 | begin, 81 | length) 82 | for name, begin, length in self._measurement_windows] 83 | 84 | @property 85 | def measurement_names(self) -> Set[str]: 86 | """Return the names of measurements that are directly declared on `self`. 87 | Does _not_ visit eventual child objects.""" 88 | return {name for name, *_ in self._measurement_windows} 89 | -------------------------------------------------------------------------------- /qupulse/pulses/plotting.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | """Deprecated plotting location. Was moved to :py:mod:`qupulse.plotting`. 6 | No deprecation warning because we will keep it around forever.""" 7 | 8 | from qupulse.plotting import * 9 | -------------------------------------------------------------------------------- /qupulse/pulses/pulse_template_parameter_mapping.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | """..deprecated:: 0.1 6 | """ 7 | 8 | from qupulse.pulses.mapping_pulse_template import MappingPulseTemplate 9 | 10 | __all__ = ["MappingPulseTemplate"] 11 | 12 | import warnings 13 | warnings.warn("MappingPulseTemplate was moved from qupulse.pulses.pulse_template_parameter_mapping to " 14 | "qupulse.pulses.mapping_pulse_template. Please consider fixing your stored pulse templates by loading " 15 | "and storing them anew.", DeprecationWarning) 16 | 17 | from qupulse.serialization import SerializableMeta 18 | SerializableMeta.deserialization_callbacks["qupulse.pulses.pulse_template_parameter_mapping.MappingPulseTemplate"] = SerializableMeta.deserialization_callbacks[MappingPulseTemplate.get_type_identifier()] 19 | -------------------------------------------------------------------------------- /qupulse/pulses/time_reversal_pulse_template.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from typing import Optional, Set, Dict, Union 6 | 7 | from qupulse import ChannelID 8 | from qupulse.program import ProgramBuilder 9 | from qupulse.program.waveforms import Waveform 10 | from qupulse.serialization import PulseRegistryType 11 | from qupulse.expressions import ExpressionScalar 12 | 13 | from qupulse.pulses.pulse_template import PulseTemplate 14 | 15 | 16 | class TimeReversalPulseTemplate(PulseTemplate): 17 | """This pulse template reverses the inner pulse template in time.""" 18 | 19 | def __init__(self, inner: PulseTemplate, 20 | identifier: Optional[str] = None, 21 | registry: PulseRegistryType = None): 22 | super(TimeReversalPulseTemplate, self).__init__(identifier=identifier) 23 | self._inner = inner 24 | self._register(registry=registry) 25 | 26 | def with_time_reversal(self) -> 'PulseTemplate': 27 | from qupulse.pulses import TimeReversalPT 28 | if self.identifier: 29 | return TimeReversalPT(self) 30 | else: 31 | return self._inner 32 | 33 | @property 34 | def parameter_names(self) -> Set[str]: 35 | return self._inner.parameter_names 36 | 37 | @property 38 | def measurement_names(self) -> Set[str]: 39 | return self._inner.measurement_names 40 | 41 | @property 42 | def duration(self) -> ExpressionScalar: 43 | return self._inner.duration 44 | 45 | @property 46 | def defined_channels(self) -> Set['ChannelID']: 47 | return self._inner.defined_channels 48 | 49 | @property 50 | def integral(self) -> Dict[ChannelID, ExpressionScalar]: 51 | return self._inner.integral 52 | 53 | def _internal_create_program(self, *, program_builder: ProgramBuilder, **kwargs) -> None: 54 | with program_builder.time_reversed() as reversed_builder: 55 | self._inner._internal_create_program(program_builder=reversed_builder, **kwargs) 56 | 57 | def build_waveform(self, 58 | *args, **kwargs) -> Optional[Waveform]: 59 | wf = self._inner.build_waveform(*args, **kwargs) 60 | if wf is not None: 61 | return wf.reversed() 62 | 63 | def get_serialization_data(self, serializer=None): 64 | assert serializer is None, "Old stype serialization not implemented for new class" 65 | return { 66 | **super().get_serialization_data(), 67 | 'inner': self._inner 68 | } 69 | 70 | def _is_atomic(self) -> bool: 71 | return self._inner._is_atomic() 72 | -------------------------------------------------------------------------------- /qupulse/utils/numeric.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from typing import Tuple, Type 6 | from numbers import Rational 7 | from math import gcd 8 | from operator import le 9 | from functools import partial 10 | 11 | import sympy 12 | 13 | 14 | try: 15 | from math import lcm 16 | except ImportError: 17 | # python version < 3.9 18 | def lcm(*integers: int): 19 | """Re-implementation of the least common multiple function that is in the standard library since python 3.9""" 20 | result = 1 21 | for value in integers: 22 | result = result * value // gcd(value, result) 23 | return result 24 | 25 | 26 | def smallest_factor_ge(n: int, min_factor: int, brute_force: int = 5): 27 | """Find the smallest factor of n that is greater or equal min_factor 28 | 29 | Args: 30 | n: number to factorize 31 | min_factor: factor must be larger this 32 | brute_force: range(min_factor, min(min_factor + brute_force)) is probed by brute force 33 | 34 | Returns: 35 | Smallest factor of n that is greater or equal min_factor 36 | """ 37 | assert min_factor <= n 38 | 39 | # this shortcut force shortcut costs 1us max 40 | for factor in range(min_factor, min(min_factor + brute_force, n)): 41 | if n % factor == 0: 42 | return factor 43 | else: 44 | return min(filter(partial(le, min_factor), 45 | sympy.ntheory.divisors(n, generator=True))) 46 | 47 | 48 | def _approximate_int(alpha_num: int, d_num: int, den: int) -> Tuple[int, int]: 49 | """Find the best fraction approximation of alpha_num / den with an error smaller d_num / den. Best means the 50 | fraction with the smallest denominator. 51 | 52 | Algorithm from https://link.springer.com/content/pdf/10.1007%2F978-3-540-72914-3.pdf 53 | 54 | Args:s 55 | alpha_num: Numerator of number to approximate. 0 < alpha_num < den 56 | d_num: Numerator of allowed absolute error. 57 | den: Denominator of both numbers above. 58 | 59 | Returns: 60 | (numerator, denominator) 61 | """ 62 | assert 0 < alpha_num < den 63 | 64 | lower_num = alpha_num - d_num 65 | upper_num = alpha_num + d_num 66 | 67 | p_a, q_a = 0, 1 68 | p_b, q_b = 1, 1 69 | 70 | p_full, q_full = p_b, q_b 71 | 72 | to_left = True 73 | 74 | while True: 75 | 76 | # compute the number of steps to the left 77 | x_num = den * p_b - alpha_num * q_b 78 | x_den = -den * p_a + alpha_num * q_a 79 | x = (x_num + x_den - 1) // x_den # ceiling division 80 | 81 | p_full += x * p_a 82 | q_full += x * q_a 83 | 84 | p_prev = p_full - p_a 85 | q_prev = q_full - q_a 86 | 87 | # check whether we have a valid approximation 88 | if (q_full * lower_num < p_full * den < q_full * upper_num or 89 | q_prev * lower_num < p_prev * den < q_prev * upper_num): 90 | bound_num = upper_num if to_left else lower_num 91 | 92 | k_num = den * p_b - bound_num * q_b 93 | k_den = bound_num * q_a - den * p_a 94 | k = (k_num // k_den) + 1 95 | 96 | return p_b + k * p_a, q_b + k * q_a 97 | 98 | # update the interval 99 | p_a = p_prev 100 | q_a = q_prev 101 | 102 | p_b = p_full 103 | q_b = q_full 104 | 105 | to_left = not to_left 106 | 107 | 108 | def approximate_rational(x: Rational, abs_err: Rational, fraction_type: Type[Rational]) -> Rational: 109 | """Return the fraction with the smallest denominator in (x - abs_err, x + abs_err)""" 110 | if abs_err <= 0: 111 | raise ValueError('abs_err must be > 0') 112 | 113 | xp, xq = x.numerator, x.denominator 114 | if xq == 1: 115 | return x 116 | 117 | dp, dq = abs_err.numerator, abs_err.denominator 118 | 119 | # separate integer part. alpha_num is guaranteed to be < xq 120 | n, alpha_num = divmod(xp, xq) 121 | 122 | # find common denominator of alpha_num / xq and dp / dq 123 | den = lcm(xq, dq) 124 | alpha_num = alpha_num * den // xq 125 | d_num = dp * den // dq 126 | 127 | if alpha_num < d_num: 128 | p, q = 0, 1 129 | else: 130 | p, q = _approximate_int(alpha_num, d_num, den) 131 | 132 | return fraction_type(p + n * q, q) 133 | 134 | 135 | def approximate_double(x: float, abs_err: float, fraction_type: Type[Rational]) -> Rational: 136 | """Return the fraction with the smallest denominator in (x - abs_err, x + abs_err).""" 137 | return approximate_rational(fraction_type(x), fraction_type(abs_err), fraction_type=fraction_type) 138 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.11" 7 | 8 | python: 9 | install: 10 | - method: pip 11 | path: . 12 | extra_requirements: 13 | - default 14 | 15 | sphinx: 16 | builder: html 17 | configuration: doc/source/conf.py 18 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import matplotlib 2 | matplotlib.use('Agg') 3 | -------------------------------------------------------------------------------- /tests/_program/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qutech/qupulse/15d67075362c3a056b942f8feb0818f48a5cc1cb/tests/_program/__init__.py -------------------------------------------------------------------------------- /tests/backward_compatibility/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qutech/qupulse/15d67075362c3a056b942f8feb0818f48a5cc1cb/tests/backward_compatibility/__init__.py -------------------------------------------------------------------------------- /tests/backward_compatibility/charge_scan_1/binary_program_validation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def set_ignored_marker_data_to_zero(wf): 5 | # np, ch, byte, data 6 | wf = wf.reshape(-1, 2, 8) 7 | 8 | channel_mask = np.uint16(2**14 - 1) 9 | 10 | wf[:, 0, :] = np.bitwise_and(wf[:, 0, :], channel_mask) 11 | 12 | 13 | def validate_programs(program_AB, program_CD, loaded_data: dict, parameters): 14 | for_A = loaded_data['for_A'] 15 | for_B = loaded_data['for_B'] 16 | for_C = loaded_data['for_C'] 17 | for_D = loaded_data['for_D'] 18 | 19 | meas_time_multiplier = parameters["charge_scan___meas_time_multiplier"] 20 | rep_count = parameters['charge_scan___rep_count'] 21 | 22 | expected_samples_A = np.tile(for_A, (meas_time_multiplier * 192, 1, rep_count)).T.ravel() 23 | set_ignored_marker_data_to_zero(expected_samples_A) 24 | samples_A = program_AB.get_as_single_waveform(0, expected_samples_A.size, with_marker=True) 25 | np.testing.assert_equal(samples_A, expected_samples_A) 26 | 27 | del samples_A 28 | del expected_samples_A 29 | 30 | expected_samples_B = np.tile(for_B, (meas_time_multiplier * 192, 1, rep_count)).T.ravel() 31 | samples_B = program_AB.get_as_single_waveform(1, expected_samples_B.size) 32 | np.testing.assert_equal(samples_B, expected_samples_B) 33 | 34 | del samples_B 35 | del expected_samples_B 36 | 37 | samples_C = program_CD.get_as_single_waveform(0) 38 | np.testing.assert_equal(samples_C, for_C) 39 | 40 | del samples_C 41 | 42 | samples_D = program_CD.get_as_single_waveform(1) 43 | np.testing.assert_equal(samples_D, for_D) 44 | -------------------------------------------------------------------------------- /tests/backward_compatibility/charge_scan_1/channel_mapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "W": "TABOR_A", 3 | "X": "TABOR_B", 4 | "Y": "TABOR_C", 5 | "Z": "TABOR_D", 6 | "marker": "TABOR_A_MARKER" 7 | } 8 | -------------------------------------------------------------------------------- /tests/backward_compatibility/charge_scan_1/hdawg_preparation_commands.json: -------------------------------------------------------------------------------- 1 | { 2 | "/{device_serial}/system/clocks/sampleclock/freq": 2e9, 3 | "/{device_serial}/awgs/*/time": 0, 4 | "/{device_serial}/sigouts/*/on": 0 5 | } -------------------------------------------------------------------------------- /tests/backward_compatibility/charge_scan_1/measurement_mapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "A": "MEAS_A", 3 | "B": "MEAS_B" 4 | } 5 | -------------------------------------------------------------------------------- /tests/backward_compatibility/charge_scan_1/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "charge_scan___N_x": 100.0, 3 | "charge_scan___N_y": 100.0, 4 | "charge_scan___W_fast": 1.0, 5 | "charge_scan___W_slow": 0.0, 6 | "charge_scan___X_fast": 0.0, 7 | "charge_scan___X_slow": 1.0, 8 | "charge_scan___Y_fast": 0.0, 9 | "charge_scan___Y_slow": 0.0, 10 | "charge_scan___Z_fast": 0.0, 11 | "charge_scan___Z_slow": 0.0, 12 | "charge_scan___meas_time_multiplier": 10, 13 | "charge_scan___rep_count": 3, 14 | "charge_scan___sample_rate": 2.0, 15 | "charge_scan___t_meas": 192.0, 16 | "charge_scan___t_wait": 0.0, 17 | "charge_scan___x_start": -0.008, 18 | "charge_scan___x_stop": 0.008, 19 | "charge_scan___y_start": -0.008, 20 | "charge_scan___y_stop": 0.008 21 | } -------------------------------------------------------------------------------- /tests/backward_compatibility/charge_scan_1/tabor_preparation_commands.json: -------------------------------------------------------------------------------- 1 | [":INST:SEL 1; :FREQ:RAST 2.0e9", ":INST:SEL 3; :FREQ:RAST 2.0e9"] 2 | -------------------------------------------------------------------------------- /tests/backward_compatibility/hardware_test_helper.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import json 4 | import typing 5 | import importlib.util 6 | import sys 7 | import warnings 8 | 9 | from qupulse.serialization import Serializer, FilesystemBackend, PulseStorage 10 | from qupulse.pulses.pulse_template import PulseTemplate 11 | 12 | class LoadingAndSequencingHelper: 13 | def __init__(self, data_folder, pulse_name): 14 | self.data_folder = data_folder 15 | self.pulse_name = pulse_name 16 | 17 | self.parameters = self.load_json('parameters.json') 18 | self.window_mapping = self.load_json('measurement_mapping.json') 19 | self.channel_mapping = self.load_json('channel_mapping.json') 20 | 21 | self.validate_programs = self.load_function_from_file('binary_program_validation.py', 'validate_programs') 22 | self.validation_data = self.load_json('binary_program_validation.json') 23 | 24 | self.pulse = None 25 | self.program = None 26 | 27 | self.simulator_manager = None 28 | 29 | self.hardware_setup = None # type: HardwareSetup 30 | self.dac = None # type: DummyDAC 31 | 32 | def load_json(self, file_name): 33 | complete_file_name = os.path.join(self.data_folder, file_name) 34 | if os.path.exists(complete_file_name): 35 | with open(complete_file_name, 'r') as file_handle: 36 | return json.load(file_handle) 37 | else: 38 | return None 39 | 40 | def load_function_from_file(self, file_name, function_name): 41 | full_file_name = os.path.join(self.data_folder, file_name) 42 | if not os.path.exists(full_file_name): 43 | return None 44 | module_name = os.path.normpath(os.path.splitext(full_file_name)[0]).replace(os.sep, '.') 45 | 46 | if module_name in sys.modules: 47 | module = sys.modules[module_name] 48 | else: 49 | try: 50 | spec = importlib.util.spec_from_file_location(module_name, full_file_name) 51 | module = importlib.util.module_from_spec(spec) 52 | spec.loader.exec_module(module) 53 | except ImportError: 54 | return None 55 | return getattr(module, function_name, None) 56 | 57 | def deserialize_pulse(self): 58 | serializer = Serializer(FilesystemBackend(os.path.join(self.data_folder, 'pulse_storage'))) 59 | self.pulse = typing.cast(PulseTemplate, serializer.deserialize(self.pulse_name)) 60 | 61 | def deserialize_pulse_2018(self) -> None: 62 | pulse_storage = PulseStorage(FilesystemBackend(os.path.join(self.data_folder, 'pulse_storage_converted_2018'))) 63 | self.pulse = typing.cast(PulseTemplate, pulse_storage[self.pulse_name]) 64 | 65 | def sequence_pulse(self): 66 | self.program = self.pulse.create_program( 67 | parameters=self.parameters, 68 | measurement_mapping=self.window_mapping, 69 | channel_mapping=self.channel_mapping) 70 | 71 | def register_program(self): 72 | self.hardware_setup.register_program(self.pulse_name, self.program) 73 | 74 | def arm_program(self): 75 | self.hardware_setup.arm_program(self.pulse_name) 76 | 77 | -------------------------------------------------------------------------------- /tests/backward_compatibility/zhinst_charge_scan_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import unittest 4 | import os 5 | import json 6 | import typing 7 | import importlib.util 8 | import sys 9 | 10 | 11 | from tests.hardware.dummy_devices import DummyDAC 12 | 13 | from qupulse.serialization import Serializer, FilesystemBackend, PulseStorage 14 | from qupulse.pulses.pulse_template import PulseTemplate 15 | from qupulse.hardware.setup import HardwareSetup, PlaybackChannel, MarkerChannel, MeasurementMask 16 | 17 | from tests.backward_compatibility.hardware_test_helper import LoadingAndSequencingHelper 18 | 19 | try: 20 | from qupulse.hardware.awgs import zihdawg 21 | except ImportError: 22 | zihdawg = None 23 | 24 | 25 | def get_test_hdawg(): 26 | assert zihdawg is not None 27 | 28 | serial_env_var = 'QUPULSE_HDAWG_TEST_DEV' 29 | interface_env_var = 'QUPULSE_HDAWG_TEST_INTERFACE' 30 | device_serial = os.environ.get(serial_env_var, None) 31 | if device_serial is None: 32 | raise unittest.SkipTest(f"No test HDAWG specified via environment variable {serial_env_var}") 33 | kwargs = dict(device_serial=device_serial) 34 | device_interface = os.environ.get(interface_env_var, None) 35 | if device_interface is not None: 36 | kwargs['device_interface'] = device_interface 37 | 38 | return zihdawg.HDAWGRepresentation(**kwargs) 39 | 40 | 41 | class HDAWGLoadingAndSequencingHelper(LoadingAndSequencingHelper): 42 | def __init__(self, data_folder, pulse_name): 43 | if zihdawg is None: 44 | raise unittest.SkipTest("zhinst import failed") 45 | 46 | super().__init__(data_folder=data_folder, pulse_name=pulse_name) 47 | 48 | self.preparation_commands = self.load_json('tabor_preparation_commands.json') 49 | 50 | self.awg: zihdawg.HDAWGRepresentation = None 51 | self.channel_group: zihdawg.HDAWGChannelGroup = None 52 | 53 | def initialize_hardware_setup(self): 54 | self.awg = get_test_hdawg() 55 | 56 | if self.preparation_commands: 57 | preparation_commands = [(key.format(device_serial=self.awg.serial), value) 58 | for key, value in self.preparation_commands.items() 59 | ] 60 | self.awg.api_session.set(preparation_commands) 61 | 62 | for idx in range(1, 9): 63 | # switch off all outputs 64 | self.awg.output(idx, False) 65 | 66 | self.awg.channel_grouping = zihdawg.HDAWGChannelGrouping.CHAN_GROUP_1x8 67 | 68 | self.channel_group, = self.awg.channel_tuples 69 | 70 | self.dac = DummyDAC() 71 | 72 | hardware_setup = HardwareSetup() 73 | 74 | hardware_setup.set_channel('TABOR_A', PlaybackChannel(self.channel_group, 0)) 75 | hardware_setup.set_channel('TABOR_B', PlaybackChannel(self.channel_group, 1)) 76 | hardware_setup.set_channel('TABOR_A_MARKER', MarkerChannel(self.channel_group, 0)) 77 | hardware_setup.set_channel('TABOR_B_MARKER', MarkerChannel(self.channel_group, 1)) 78 | 79 | hardware_setup.set_channel('TABOR_C', PlaybackChannel(self.channel_group, 2)) 80 | hardware_setup.set_channel('TABOR_D', PlaybackChannel(self.channel_group, 3)) 81 | hardware_setup.set_channel('TABOR_C_MARKER', MarkerChannel(self.channel_group, 3)) 82 | hardware_setup.set_channel('TABOR_D_MARKER', MarkerChannel(self.channel_group, 4)) 83 | 84 | hardware_setup.set_measurement("MEAS_A", MeasurementMask(self.dac, "MASK_A")) 85 | hardware_setup.set_measurement("MEAS_B", MeasurementMask(self.dac, "MASK_B")) 86 | hardware_setup.set_measurement("MEAS_C", MeasurementMask(self.dac, "MASK_C")) 87 | hardware_setup.set_measurement("MEAS_D", MeasurementMask(self.dac, "MASK_D")) 88 | 89 | self.hardware_setup = hardware_setup 90 | 91 | 92 | class CompleteIntegrationTestHelper(unittest.TestCase): 93 | data_folder = None 94 | pulse_name = None 95 | 96 | @classmethod 97 | def setUpClass(cls): 98 | if cls.data_folder is None: 99 | raise unittest.SkipTest("Base class") 100 | cls.test_state = HDAWGLoadingAndSequencingHelper(cls.data_folder, cls.pulse_name) 101 | 102 | def test_1_1_deserialization(self): 103 | with self.assertWarns(DeprecationWarning): 104 | self.test_state.deserialize_pulse() 105 | 106 | def test_1_2_deserialization_2018(self) -> None: 107 | self.test_state.deserialize_pulse_2018() 108 | 109 | def test_2_1_sequencing(self): 110 | if self.test_state.pulse is None: 111 | self.skipTest("deserialization failed") 112 | self.test_state.sequence_pulse() 113 | 114 | def test_3_1_initialize_hardware_setup(self): 115 | self.test_state.initialize_hardware_setup() 116 | 117 | def test_4_1_register_program(self): 118 | if self.test_state.hardware_setup is None: 119 | self.skipTest("No hardware setup") 120 | self.test_state.register_program() 121 | self.assertIn(self.pulse_name, self.test_state.hardware_setup.registered_programs) 122 | 123 | def test_5_1_arm_program(self): 124 | if self.test_state.hardware_setup is None: 125 | self.skipTest("No hardware setup") 126 | if self.pulse_name not in self.test_state.hardware_setup.registered_programs: 127 | self.skipTest("Program is not registered") 128 | self.test_state.hardware_setup.arm_program(self.pulse_name) 129 | self.assertEqual(self.test_state.channel_group._current_program, self.pulse_name, 130 | "Program not armed") 131 | 132 | 133 | class ChargeScan1Tests(CompleteIntegrationTestHelper): 134 | data_folder = os.path.join(os.path.dirname(__file__), 'charge_scan_1') 135 | pulse_name = 'charge_scan' 136 | -------------------------------------------------------------------------------- /tests/comparable_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from typing import Any 3 | import warnings 4 | 5 | with warnings.catch_warnings(): 6 | warnings.simplefilter(action='ignore', category=DeprecationWarning) 7 | from qupulse.comparable import Comparable 8 | 9 | class DummyComparable(Comparable): 10 | 11 | def __init__(self, compare_key: Any) -> None: 12 | super().__init__() 13 | self.compare_key_ = compare_key 14 | 15 | @property 16 | def compare_key(self) -> Any: 17 | return self.compare_key_ 18 | 19 | 20 | class ComparableTests(unittest.TestCase): 21 | 22 | def test_hash(self) -> None: 23 | comp_a = DummyComparable(17) 24 | self.assertEqual(hash(17), hash(comp_a)) 25 | 26 | def test_eq(self) -> None: 27 | comp_a = DummyComparable(17) 28 | comp_b = DummyComparable(18) 29 | comp_c = DummyComparable(18) 30 | self.assertNotEqual(comp_a, comp_b) 31 | self.assertNotEqual(comp_b, comp_a) 32 | self.assertEqual(comp_b, comp_c) 33 | self.assertNotEqual(comp_a, "foo") 34 | self.assertNotEqual("foo", comp_a) 35 | -------------------------------------------------------------------------------- /tests/expressions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qutech/qupulse/15d67075362c3a056b942f8feb0818f48a5cc1cb/tests/expressions/__init__.py -------------------------------------------------------------------------------- /tests/hardware/__init__.py: -------------------------------------------------------------------------------- 1 | """Import dummy packages for non-available drivers""" 2 | from tests.hardware.dummy_modules import import_package 3 | 4 | try: 5 | import atsaverage 6 | except ImportError: 7 | atsaverage = import_package('atsaverage') 8 | -------------------------------------------------------------------------------- /tests/hardware/base_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | from collections import OrderedDict 4 | 5 | import numpy as np 6 | 7 | from qupulse.utils.types import TimeType 8 | from qupulse.program.loop import Loop 9 | from qupulse.hardware.awgs.base import ProgramEntry 10 | 11 | from tests.pulses.sequencing_dummies import DummyWaveform 12 | 13 | 14 | class ProgramEntryTests(unittest.TestCase): 15 | def setUp(self) -> None: 16 | self.channels = ('A', None, 'C') 17 | self.marker = (None, 'M') 18 | self.amplitudes = (1., 1., .5) 19 | self.offset = (0., .5, .1) 20 | self.voltage_transformations = ( 21 | mock.Mock(wraps=lambda x: x), 22 | mock.Mock(wraps=lambda x: x), 23 | mock.Mock(wraps=lambda x: x) 24 | ) 25 | self.sample_rate = TimeType.from_float(1) 26 | 27 | N = 100 28 | t = np.arange(N) 29 | 30 | self.sampled = [ 31 | dict(A=np.linspace(-.1, .1, num=N), C=.1*np.sin(t), M=np.arange(N) % 2), 32 | dict(A=np.linspace(.1, -.1, num=N//2), C=.1 * np.cos(t[::2]), M=np.arange(N//2) % 3) 33 | ] 34 | self.waveforms = [ 35 | wf 36 | for wf in (DummyWaveform(sample_output=sampled, duration=sampled['A'].size) for sampled in self.sampled) 37 | ] 38 | self.loop = Loop(children=[Loop(waveform=wf) for wf in self.waveforms] * 2) 39 | 40 | def test_init(self): 41 | sampled = [mock.Mock(), mock.Mock()] 42 | expected_default = OrderedDict([(wf, None) for wf in self.waveforms]).keys() 43 | expected_waveforms = OrderedDict(zip(self.waveforms, sampled)) 44 | 45 | with mock.patch.object(ProgramEntry, '_sample_waveforms', return_value=sampled) as sample_waveforms: 46 | entry = ProgramEntry(program=self.loop, 47 | channels=self.channels, 48 | markers=self.marker, 49 | amplitudes=self.amplitudes, 50 | offsets=self.offset, 51 | voltage_transformations=self.voltage_transformations, 52 | sample_rate=self.sample_rate, 53 | waveforms=[]) 54 | self.assertIs(self.loop, entry._loop) 55 | self.assertEqual(0, len(entry._waveforms)) 56 | sample_waveforms.assert_not_called() 57 | 58 | with mock.patch.object(ProgramEntry, '_sample_waveforms', return_value=sampled) as sample_waveforms: 59 | entry = ProgramEntry(program=self.loop, 60 | channels=self.channels, 61 | markers=self.marker, 62 | amplitudes=self.amplitudes, 63 | offsets=self.offset, 64 | voltage_transformations=self.voltage_transformations, 65 | sample_rate=self.sample_rate, 66 | waveforms=None) 67 | self.assertEqual(expected_waveforms, entry._waveforms) 68 | sample_waveforms.assert_called_once_with(expected_default) 69 | 70 | with mock.patch.object(ProgramEntry, '_sample_waveforms', return_value=sampled[:1]) as sample_waveforms: 71 | entry = ProgramEntry(program=self.loop, 72 | channels=self.channels, 73 | markers=self.marker, 74 | amplitudes=self.amplitudes, 75 | offsets=self.offset, 76 | voltage_transformations=self.voltage_transformations, 77 | sample_rate=self.sample_rate, 78 | waveforms=self.waveforms[:1]) 79 | self.assertEqual(OrderedDict([(self.waveforms[0], sampled[0])]), entry._waveforms) 80 | sample_waveforms.assert_called_once_with(self.waveforms[:1]) 81 | 82 | def test_sample_waveforms(self): 83 | empty_ch = np.array([1, 2, 3]) 84 | empty_m = np.array([0, 1, 0]) 85 | # channels == (A, None, C) 86 | 87 | expected_sampled = [ 88 | ((self.sampled[0]['A'], empty_ch, 2.*(self.sampled[0]['C'] - 0.1)), (empty_m, self.sampled[0]['M'] != 0)), 89 | ((self.sampled[1]['A'], empty_ch, 2.*(self.sampled[1]['C'] - 0.1)), (empty_m, self.sampled[1]['M'] != 0)) 90 | ] 91 | 92 | entry = ProgramEntry(program=self.loop, 93 | channels=self.channels, 94 | markers=self.marker, 95 | amplitudes=self.amplitudes, 96 | offsets=self.offset, 97 | voltage_transformations=self.voltage_transformations, 98 | sample_rate=self.sample_rate, 99 | waveforms=[]) 100 | 101 | with mock.patch.object(entry, '_sample_empty_channel', return_value=empty_ch): 102 | with mock.patch.object(entry, '_sample_empty_marker', return_value=empty_m): 103 | sampled = entry._sample_waveforms(self.waveforms) 104 | np.testing.assert_equal(expected_sampled, sampled) 105 | -------------------------------------------------------------------------------- /tests/hardware/dummy_devices.py: -------------------------------------------------------------------------------- 1 | from qupulse.hardware.dacs.dummy import DummyDAC 2 | from qupulse.hardware.awgs.dummy import DummyAWG 3 | -------------------------------------------------------------------------------- /tests/hardware/dummy_modules.py: -------------------------------------------------------------------------------- 1 | """Import dummy modules if actual modules not installed. Sets dummy modules in sys so subsequent imports 2 | use the dummies""" 3 | 4 | import sys 5 | from typing import Set 6 | import unittest.mock 7 | 8 | class dummy_package: 9 | pass 10 | 11 | 12 | class dummy_atsaverage(dummy_package): 13 | class atsaverage(dummy_package): 14 | pass 15 | class alazar(dummy_package): 16 | pass 17 | class core(dummy_package): 18 | class AlazarCard: 19 | model = 'DUMMY' 20 | minimum_record_size = 256 21 | def __init__(self): 22 | self._startAcquisition_calls = [] 23 | self._applyConfiguration_calls = [] 24 | def startAcquisition(self, x: int): 25 | self._startAcquisition_calls.append(x) 26 | def applyConfiguration(self, config): 27 | self._applyConfiguration_calls.append(config) 28 | class config(dummy_package): 29 | class CaptureClockConfig: 30 | def numeric_sample_rate(self, card): 31 | return 10**8 32 | class ScanlineConfiguration: 33 | def __init__(self): 34 | self._apply_calls = [] 35 | def apply(self, card, print_debug_output): 36 | self._apply_calls.append((card, print_debug_output)) 37 | aimedBufferSize = unittest.mock.PropertyMock(return_value=2**22) 38 | ScanlineConfiguration.captureClockConfiguration = CaptureClockConfig() 39 | class operations(dummy_package): 40 | class OperationDefinition: 41 | pass 42 | class masks(dummy_package): 43 | class Mask: 44 | pass 45 | class CrossBufferMask: 46 | pass 47 | 48 | 49 | def import_package(name, package=None) -> Set[dummy_package]: 50 | if package is None: 51 | package_dict = dict(atsaverage=dummy_atsaverage) 52 | if name in package_dict: 53 | package = package_dict[name] 54 | else: 55 | raise KeyError('Unknown package', name) 56 | 57 | imported = set() 58 | sys.modules[name] = package 59 | imported.add(package) 60 | for attr in dir(package): 61 | if isinstance(getattr(package, attr), type) and issubclass(getattr(package, attr), dummy_package): 62 | imported |= import_package(name + '.' + attr, getattr(package, attr)) 63 | return imported 64 | 65 | 66 | def replace_missing(): 67 | failed_imports = set() 68 | 69 | try: 70 | import atsaverage 71 | import atsaverage.config 72 | except ImportError: 73 | failed_imports |= import_package('atsaverage', dummy_atsaverage) 74 | return failed_imports 75 | 76 | -------------------------------------------------------------------------------- /tests/hardware/feature_awg/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qutech/qupulse/15d67075362c3a056b942f8feb0818f48a5cc1cb/tests/hardware/feature_awg/__init__.py -------------------------------------------------------------------------------- /tests/hardware/feature_awg/channel_tuple_wrapper_tests.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase, mock 2 | 3 | from qupulse.hardware.feature_awg.features import ProgramManagement, VolatileParameters 4 | 5 | from tests.hardware.feature_awg.awg_new_driver_base_tests import DummyAWGChannelTuple, DummyAWGDevice, DummyAWGChannel, DummyVolatileParameters 6 | 7 | 8 | class ChannelTupleAdapterTest(TestCase): 9 | def setUp(self): 10 | self.device = DummyAWGDevice("device") 11 | self.channels = [DummyAWGChannel(0, self.device), DummyAWGChannel(1, self.device)] 12 | self.tuple = DummyAWGChannelTuple(0, device=self.device, channels=self.channels) 13 | 14 | def test_simple_properties(self): 15 | adapter = self.tuple.channel_tuple_adapter 16 | self.assertEqual(adapter.num_channels, len(self.channels)) 17 | self.assertEqual(adapter.num_markers, 0) 18 | self.assertEqual(adapter.identifier, self.tuple.name) 19 | self.assertEqual(adapter.sample_rate, self.tuple.sample_rate) 20 | 21 | def test_upload(self): 22 | adapter = self.tuple.channel_tuple_adapter 23 | 24 | upload_kwargs = dict(name="upload_test", 25 | program=mock.Mock(), 26 | channels=('A', None), 27 | voltage_transformation=(lambda x:x, lambda x:x**2), 28 | markers=(), force=True) 29 | 30 | expected_kwargs = {**upload_kwargs, 'repetition_mode': None} 31 | expected_kwargs['marker_channels'] = expected_kwargs.pop('markers') 32 | 33 | with mock.patch.object(self.tuple[ProgramManagement], 'upload') as upload_mock: 34 | adapter.upload(**upload_kwargs) 35 | upload_mock.assert_called_once_with(**expected_kwargs) 36 | 37 | def test_arm(self): 38 | adapter = self.tuple.channel_tuple_adapter 39 | with mock.patch.object(self.tuple[ProgramManagement], 'arm') as arm_mock: 40 | adapter.arm('test_prog') 41 | arm_mock.assert_called_once_with('test_prog') 42 | 43 | def test_remove(self): 44 | adapter = self.tuple.channel_tuple_adapter 45 | with mock.patch.object(self.tuple[ProgramManagement], 'remove') as remove_mock: 46 | adapter.remove('test_prog') 47 | remove_mock.assert_called_once_with('test_prog') 48 | 49 | def test_clear(self): 50 | adapter = self.tuple.channel_tuple_adapter 51 | with mock.patch.object(self.tuple[ProgramManagement], 'clear') as clear_mock: 52 | adapter.clear() 53 | clear_mock.assert_called_once_with() 54 | 55 | def test_programs(self): 56 | adapter = self.tuple.channel_tuple_adapter 57 | with mock.patch.object(type(self.tuple[ProgramManagement]), 'programs', new_callable=mock.PropertyMock): 58 | self.assertIs(self.tuple[ProgramManagement].programs, adapter.programs) 59 | 60 | def test_set_volatile_parameters(self): 61 | adapter = self.tuple.channel_tuple_adapter 62 | 63 | self.tuple.add_feature(DummyVolatileParameters(self.tuple)) 64 | 65 | with mock.patch.object(self.tuple[VolatileParameters], 'set_volatile_parameters') as set_volatile_parameters_mock: 66 | adapter.set_volatile_parameters('wurst', {'a': 5.}) 67 | set_volatile_parameters_mock.assert_called_once_with('wurst', {'a': 5.}) 68 | -------------------------------------------------------------------------------- /tests/hardware/feature_awg/tabor_new_driver_clock_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | with_alazar = True 5 | 6 | def get_pulse(): 7 | from qupulse.pulses import TablePulseTemplate as TPT, SequencePulseTemplate as SPT, RepetitionPulseTemplate as RPT 8 | 9 | ramp = TPT(identifier='ramp', channels={'out', 'trigger'}) 10 | ramp.add_entry(0, 'start', channel='out') 11 | ramp.add_entry('duration', 'stop', 'linear', channel='out') 12 | 13 | ramp.add_entry(0, 1, channel='trigger') 14 | ramp.add_entry('duration', 1, 'hold', channel='trigger') 15 | 16 | ramp.add_measurement_declaration('meas', 0, 'duration') 17 | 18 | base = SPT([(ramp, dict(start='min', stop='max', duration='tau/3'), dict(meas='A')), 19 | (ramp, dict(start='max', stop='max', duration='tau/3'), dict(meas='B')), 20 | (ramp, dict(start='max', stop='min', duration='tau/3'), dict(meas='C'))], {'min', 'max', 'tau'}) 21 | 22 | repeated = RPT(base, 'n') 23 | 24 | root = SPT([repeated, repeated, repeated], {'min', 'max', 'tau', 'n'}) 25 | 26 | return root 27 | 28 | 29 | def get_alazar_config(): 30 | from atsaverage import alazar 31 | from atsaverage.config import ScanlineConfiguration, CaptureClockConfiguration, EngineTriggerConfiguration,\ 32 | TRIGInputConfiguration, InputConfiguration 33 | 34 | trig_level = int((5 + 0.4) / 10. * 255) 35 | assert 0 <= trig_level < 256 36 | 37 | config = ScanlineConfiguration() 38 | config.triggerInputConfiguration = TRIGInputConfiguration(triggerRange=alazar.TriggerRangeID.etr_5V) 39 | config.triggerConfiguration = EngineTriggerConfiguration(triggerOperation=alazar.TriggerOperation.J, 40 | triggerEngine1=alazar.TriggerEngine.J, 41 | triggerSource1=alazar.TriggerSource.external, 42 | triggerSlope1=alazar.TriggerSlope.positive, 43 | triggerLevel1=trig_level, 44 | triggerEngine2=alazar.TriggerEngine.K, 45 | triggerSource2=alazar.TriggerSource.disable, 46 | triggerSlope2=alazar.TriggerSlope.positive, 47 | triggerLevel2=trig_level) 48 | config.captureClockConfiguration = CaptureClockConfiguration(source=alazar.CaptureClockType.internal_clock, 49 | samplerate=alazar.SampleRateID.rate_100MSPS) 50 | config.inputConfiguration = 4*[InputConfiguration(input_range=alazar.InputRangeID.range_1_V)] 51 | config.totalRecordSize = 0 52 | 53 | assert config.totalRecordSize == 0 54 | 55 | return config 56 | 57 | def get_operations(): 58 | from atsaverage.operations import Downsample 59 | 60 | return [Downsample(identifier='DS_A', maskID='A'), 61 | Downsample(identifier='DS_B', maskID='B'), 62 | Downsample(identifier='DS_C', maskID='C'), 63 | Downsample(identifier='DS_D', maskID='D')] 64 | 65 | def get_window(card): 66 | from atsaverage.gui import ThreadedStatusWindow 67 | window = ThreadedStatusWindow(card) 68 | window.start() 69 | return window 70 | 71 | 72 | class TaborTests(unittest.TestCase): 73 | @unittest.skip 74 | def test_all(self): 75 | from qupulse.hardware.feature_awg.tabor import TaborChannelTuple, TaborDevice 76 | #import warnings 77 | tawg = TaborDevice(r'USB0::0x168C::0x2184::0000216488::INSTR') 78 | tchannelpair = TaborChannelTuple(tawg, (1, 2), 'TABOR_AB') 79 | tawg.paranoia_level = 2 80 | 81 | #warnings.simplefilter('error', Warning) 82 | 83 | from qupulse.hardware.setup import HardwareSetup, PlaybackChannel, MarkerChannel 84 | hardware_setup = HardwareSetup() 85 | 86 | hardware_setup.set_channel('TABOR_A', PlaybackChannel(tchannelpair, 0)) 87 | hardware_setup.set_channel('TABOR_B', PlaybackChannel(tchannelpair, 1)) 88 | hardware_setup.set_channel('TABOR_A_MARKER', MarkerChannel(tchannelpair, 0)) 89 | hardware_setup.set_channel('TABOR_B_MARKER', MarkerChannel(tchannelpair, 1)) 90 | 91 | if with_alazar: 92 | from qupulse.hardware.dacs.alazar import AlazarCard 93 | import atsaverage.server 94 | 95 | if not atsaverage.server.Server.default_instance.running: 96 | atsaverage.server.Server.default_instance.start(key=b'guest') 97 | 98 | import atsaverage.core 99 | 100 | alazar = AlazarCard(atsaverage.core.getLocalCard(1, 1)) 101 | alazar.register_mask_for_channel('A', 0) 102 | alazar.register_mask_for_channel('B', 0) 103 | alazar.register_mask_for_channel('C', 0) 104 | alazar.config = get_alazar_config() 105 | 106 | alazar.register_operations('test', get_operations()) 107 | window = get_window(atsaverage.core.getLocalCard(1, 1)) 108 | hardware_setup.register_dac(alazar) 109 | 110 | repeated = get_pulse() 111 | 112 | from qupulse.pulses.sequencing import Sequencer 113 | 114 | sequencer = Sequencer() 115 | sequencer.push(repeated, 116 | parameters=dict(n=1000, min=-0.5, max=0.5, tau=192*3), 117 | channel_mapping={'out': 'TABOR_A', 'trigger': 'TABOR_A_MARKER'}, 118 | window_mapping=dict(A='A', B='B', C='C')) 119 | instruction_block = sequencer.build() 120 | 121 | hardware_setup.register_program('test', instruction_block) 122 | 123 | if with_alazar: 124 | from atsaverage.masks import PeriodicMask 125 | m = PeriodicMask() 126 | m.identifier = 'D' 127 | m.begin = 0 128 | m.end = 1 129 | m.period = 1 130 | m.channel = 0 131 | alazar._registered_programs['test'].masks.append(m) 132 | 133 | hardware_setup.arm_program('test') 134 | 135 | d = 1 136 | -------------------------------------------------------------------------------- /tests/hardware/feature_awg/tabor_new_driver_exex_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | with_alazar = True 5 | 6 | def get_pulse(): 7 | from qupulse.pulses import TablePulseTemplate as TPT, SequencePulseTemplate as SPT, RepetitionPulseTemplate as RPT 8 | 9 | ramp = TPT(identifier='ramp', channels={'out', 'trigger'}) 10 | ramp.add_entry(0, 'start', channel='out') 11 | ramp.add_entry('duration', 'stop', 'linear', channel='out') 12 | 13 | ramp.add_entry(0, 1, channel='trigger') 14 | ramp.add_entry('duration', 1, 'hold', channel='trigger') 15 | 16 | ramp.add_measurement_declaration('meas', 0, 'duration') 17 | 18 | base = SPT([(ramp, dict(start='min', stop='max', duration='tau/3'), dict(meas='A')), 19 | (ramp, dict(start='max', stop='max', duration='tau/3'), dict(meas='B')), 20 | (ramp, dict(start='max', stop='min', duration='tau/3'), dict(meas='C'))], {'min', 'max', 'tau'}) 21 | 22 | repeated = RPT(base, 'n') 23 | 24 | root = SPT([repeated, repeated, repeated], {'min', 'max', 'tau', 'n'}) 25 | 26 | return root 27 | 28 | 29 | def get_alazar_config(): 30 | from atsaverage import alazar 31 | from atsaverage.config import ScanlineConfiguration, CaptureClockConfiguration, EngineTriggerConfiguration,\ 32 | TRIGInputConfiguration, InputConfiguration 33 | 34 | trig_level = int((5 + 0.4) / 10. * 255) 35 | assert 0 <= trig_level < 256 36 | 37 | config = ScanlineConfiguration() 38 | config.triggerInputConfiguration = TRIGInputConfiguration(triggerRange=alazar.TriggerRangeID.etr_5V) 39 | config.triggerConfiguration = EngineTriggerConfiguration(triggerOperation=alazar.TriggerOperation.J, 40 | triggerEngine1=alazar.TriggerEngine.J, 41 | triggerSource1=alazar.TriggerSource.external, 42 | triggerSlope1=alazar.TriggerSlope.positive, 43 | triggerLevel1=trig_level, 44 | triggerEngine2=alazar.TriggerEngine.K, 45 | triggerSource2=alazar.TriggerSource.disable, 46 | triggerSlope2=alazar.TriggerSlope.positive, 47 | triggerLevel2=trig_level) 48 | config.captureClockConfiguration = CaptureClockConfiguration(source=alazar.CaptureClockType.internal_clock, 49 | samplerate=alazar.SampleRateID.rate_100MSPS) 50 | config.inputConfiguration = 4*[InputConfiguration(input_range=alazar.InputRangeID.range_1_V)] 51 | config.totalRecordSize = 0 52 | 53 | assert config.totalRecordSize == 0 54 | 55 | return config 56 | 57 | def get_operations(): 58 | from atsaverage.operations import Downsample 59 | 60 | return [Downsample(identifier='DS_A', maskID='A'), 61 | Downsample(identifier='DS_B', maskID='B'), 62 | Downsample(identifier='DS_C', maskID='C'), 63 | Downsample(identifier='DS_D', maskID='D')] 64 | 65 | def get_window(card): 66 | from atsaverage.gui import ThreadedStatusWindow 67 | window = ThreadedStatusWindow(card) 68 | window.start() 69 | return window 70 | 71 | 72 | class TaborTests(unittest.TestCase): 73 | @unittest.skip 74 | def test_all(self): 75 | from qupulse.hardware.feature_awg.tabor import TaborChannelTuple, TaborDevice 76 | #import warnings 77 | tawg = TaborDevice(r'USB0::0x168C::0x2184::0000216488::INSTR') 78 | tchannelpair = TaborChannelTuple(tawg, (1, 2), 'TABOR_AB') 79 | tawg.paranoia_level = 2 80 | 81 | #warnings.simplefilter('error', Warning) 82 | 83 | from qupulse.hardware.setup import HardwareSetup, PlaybackChannel, MarkerChannel 84 | hardware_setup = HardwareSetup() 85 | 86 | hardware_setup.set_channel('TABOR_A', PlaybackChannel(tchannelpair, 0)) 87 | hardware_setup.set_channel('TABOR_B', PlaybackChannel(tchannelpair, 1)) 88 | hardware_setup.set_channel('TABOR_A_MARKER', MarkerChannel(tchannelpair, 0)) 89 | hardware_setup.set_channel('TABOR_B_MARKER', MarkerChannel(tchannelpair, 1)) 90 | 91 | if with_alazar: 92 | from qupulse.hardware.dacs.alazar import AlazarCard 93 | import atsaverage.server 94 | 95 | if not atsaverage.server.Server.default_instance.running: 96 | atsaverage.server.Server.default_instance.start(key=b'guest') 97 | 98 | import atsaverage.core 99 | 100 | alazar = AlazarCard(atsaverage.core.getLocalCard(1, 1)) 101 | alazar.register_mask_for_channel('A', 0) 102 | alazar.register_mask_for_channel('B', 0) 103 | alazar.register_mask_for_channel('C', 0) 104 | alazar.config = get_alazar_config() 105 | 106 | alazar.register_operations('test', get_operations()) 107 | window = get_window(atsaverage.core.getLocalCard(1, 1)) 108 | hardware_setup.register_dac(alazar) 109 | 110 | repeated = get_pulse() 111 | 112 | from qupulse.pulses.sequencing import Sequencer 113 | 114 | sequencer = Sequencer() 115 | sequencer.push(repeated, 116 | parameters=dict(n=1000, min=-0.5, max=0.5, tau=192*3), 117 | channel_mapping={'out': 'TABOR_A', 'trigger': 'TABOR_A_MARKER'}, 118 | window_mapping=dict(A='A', B='B', C='C')) 119 | instruction_block = sequencer.build() 120 | 121 | hardware_setup.register_program('test', instruction_block) 122 | 123 | if with_alazar: 124 | from atsaverage.masks import PeriodicMask 125 | m = PeriodicMask() 126 | m.identifier = 'D' 127 | m.begin = 0 128 | m.end = 1 129 | m.period = 1 130 | m.channel = 0 131 | alazar._registered_programs['test'].masks.append(m) 132 | 133 | hardware_setup.arm_program('test') 134 | 135 | d = 1 136 | 137 | -------------------------------------------------------------------------------- /tests/hardware/feature_awg/tabor_new_driver_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | try: 4 | import tabor_control 5 | except ImportError as err: 6 | raise unittest.SkipTest("tabor_control not present") from err 7 | 8 | from qupulse.hardware.feature_awg.tabor import with_configuration_guard 9 | 10 | 11 | class ConfigurationGuardTest(unittest.TestCase): 12 | class DummyChannelPair: 13 | def __init__(self, test_obj: unittest.TestCase): 14 | self.test_obj = test_obj 15 | self._configuration_guard_count = 0 16 | self.is_in_config_mode = False 17 | 18 | def _enter_config_mode(self): 19 | self.test_obj.assertFalse(self.is_in_config_mode) 20 | self.test_obj.assertEqual(self._configuration_guard_count, 0) 21 | self.is_in_config_mode = True 22 | 23 | def _exit_config_mode(self): 24 | self.test_obj.assertTrue(self.is_in_config_mode) 25 | self.test_obj.assertEqual(self._configuration_guard_count, 0) 26 | self.is_in_config_mode = False 27 | 28 | @with_configuration_guard 29 | def guarded_method(self, counter=5, throw=False): 30 | self.test_obj.assertTrue(self.is_in_config_mode) 31 | if counter > 0: 32 | return self.guarded_method(counter - 1, throw) + 1 33 | if throw: 34 | raise RuntimeError() 35 | return 0 36 | 37 | def test_config_guard(self): 38 | channel_pair = ConfigurationGuardTest.DummyChannelPair(self) 39 | 40 | for i in range(5): 41 | self.assertEqual(channel_pair.guarded_method(i), i) 42 | 43 | with self.assertRaises(RuntimeError): 44 | channel_pair.guarded_method(1, True) 45 | 46 | self.assertFalse(channel_pair.is_in_config_mode) 47 | 48 | 49 | -------------------------------------------------------------------------------- /tests/hardware/tabor_exex_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | with_alazar = True 5 | 6 | def get_pulse(): 7 | from qupulse.pulses import TablePulseTemplate as TPT, SequencePulseTemplate as SPT, RepetitionPulseTemplate as RPT 8 | 9 | ramp = TPT(identifier='ramp', channels={'out', 'trigger'}) 10 | ramp.add_entry(0, 'start', channel='out') 11 | ramp.add_entry('duration', 'stop', 'linear', channel='out') 12 | 13 | ramp.add_entry(0, 1, channel='trigger') 14 | ramp.add_entry('duration', 1, 'hold', channel='trigger') 15 | 16 | ramp.add_measurement_declaration('meas', 0, 'duration') 17 | 18 | base = SPT([(ramp, dict(start='min', stop='max', duration='tau/3'), dict(meas='A')), 19 | (ramp, dict(start='max', stop='max', duration='tau/3'), dict(meas='B')), 20 | (ramp, dict(start='max', stop='min', duration='tau/3'), dict(meas='C'))], {'min', 'max', 'tau'}) 21 | 22 | repeated = RPT(base, 'n') 23 | 24 | root = SPT([repeated, repeated, repeated], {'min', 'max', 'tau', 'n'}) 25 | 26 | return root 27 | 28 | 29 | def get_alazar_config(): 30 | from atsaverage import alazar 31 | from atsaverage.config import ScanlineConfiguration, CaptureClockConfiguration, EngineTriggerConfiguration,\ 32 | TRIGInputConfiguration, InputConfiguration 33 | 34 | trig_level = int((5 + 0.4) / 10. * 255) 35 | assert 0 <= trig_level < 256 36 | 37 | config = ScanlineConfiguration() 38 | config.triggerInputConfiguration = TRIGInputConfiguration(triggerRange=alazar.TriggerRangeID.etr_5V) 39 | config.triggerConfiguration = EngineTriggerConfiguration(triggerOperation=alazar.TriggerOperation.J, 40 | triggerEngine1=alazar.TriggerEngine.J, 41 | triggerSource1=alazar.TriggerSource.external, 42 | triggerSlope1=alazar.TriggerSlope.positive, 43 | triggerLevel1=trig_level, 44 | triggerEngine2=alazar.TriggerEngine.K, 45 | triggerSource2=alazar.TriggerSource.disable, 46 | triggerSlope2=alazar.TriggerSlope.positive, 47 | triggerLevel2=trig_level) 48 | config.captureClockConfiguration = CaptureClockConfiguration(source=alazar.CaptureClockType.internal_clock, 49 | samplerate=alazar.SampleRateID.rate_100MSPS) 50 | config.inputConfiguration = 4*[InputConfiguration(input_range=alazar.InputRangeID.range_1_V)] 51 | config.totalRecordSize = 0 52 | 53 | assert config.totalRecordSize == 0 54 | 55 | return config 56 | 57 | def get_operations(): 58 | from atsaverage.operations import Downsample 59 | 60 | return [Downsample(identifier='DS_A', maskID='A'), 61 | Downsample(identifier='DS_B', maskID='B'), 62 | Downsample(identifier='DS_C', maskID='C'), 63 | Downsample(identifier='DS_D', maskID='D')] 64 | 65 | def get_window(card): 66 | from atsaverage.gui import ThreadedStatusWindow 67 | window = ThreadedStatusWindow(card) 68 | window.start() 69 | return window 70 | 71 | 72 | class TaborTests(unittest.TestCase): 73 | @unittest.skip 74 | def test_all(self): 75 | from qupulse.hardware.awgs.tabor import TaborChannelPair, TaborAWGRepresentation 76 | #import warnings 77 | tawg = TaborAWGRepresentation(r'USB0::0x168C::0x2184::0000216488::INSTR') 78 | tchannelpair = TaborChannelPair(tawg, (1, 2), 'TABOR_AB') 79 | tawg.paranoia_level = 2 80 | 81 | #warnings.simplefilter('error', Warning) 82 | 83 | from qupulse.hardware.setup import HardwareSetup, PlaybackChannel, MarkerChannel 84 | hardware_setup = HardwareSetup() 85 | 86 | hardware_setup.set_channel('TABOR_A', PlaybackChannel(tchannelpair, 0)) 87 | hardware_setup.set_channel('TABOR_B', PlaybackChannel(tchannelpair, 1)) 88 | hardware_setup.set_channel('TABOR_A_MARKER', MarkerChannel(tchannelpair, 0)) 89 | hardware_setup.set_channel('TABOR_B_MARKER', MarkerChannel(tchannelpair, 1)) 90 | 91 | if with_alazar: 92 | from qupulse.hardware.dacs.alazar import AlazarCard 93 | import atsaverage.server 94 | 95 | if not atsaverage.server.Server.default_instance.running: 96 | atsaverage.server.Server.default_instance.start(key=b'guest') 97 | 98 | import atsaverage.core 99 | 100 | alazar = AlazarCard(atsaverage.core.getLocalCard(1, 1)) 101 | alazar.register_mask_for_channel('A', 0) 102 | alazar.register_mask_for_channel('B', 0) 103 | alazar.register_mask_for_channel('C', 0) 104 | alazar.config = get_alazar_config() 105 | 106 | alazar.register_operations('test', get_operations()) 107 | window = get_window(atsaverage.core.getLocalCard(1, 1)) 108 | hardware_setup.register_dac(alazar) 109 | 110 | repeated = get_pulse() 111 | 112 | from qupulse.pulses.sequencing import Sequencer 113 | 114 | sequencer = Sequencer() 115 | sequencer.push(repeated, 116 | parameters=dict(n=1000, min=-0.5, max=0.5, tau=192*3), 117 | channel_mapping={'out': 'TABOR_A', 'trigger': 'TABOR_A_MARKER'}, 118 | window_mapping=dict(A='A', B='B', C='C')) 119 | instruction_block = sequencer.build() 120 | 121 | hardware_setup.register_program('test', instruction_block) 122 | 123 | if with_alazar: 124 | from atsaverage.masks import PeriodicMask 125 | m = PeriodicMask() 126 | m.identifier = 'D' 127 | m.begin = 0 128 | m.end = 1 129 | m.period = 1 130 | m.channel = 0 131 | alazar._registered_programs['test'].masks.append(m) 132 | 133 | hardware_setup.arm_program('test') 134 | 135 | d = 1 136 | 137 | -------------------------------------------------------------------------------- /tests/hardware/tabor_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import itertools 3 | import numpy as np 4 | 5 | try: 6 | import tabor_control 7 | except ImportError as err: 8 | raise unittest.SkipTest("tabor_control not present") from err 9 | 10 | from qupulse.hardware.awgs.tabor import TaborException, TaborProgram, \ 11 | TaborSegment, TaborSequencing, with_configuration_guard, PlottableProgram 12 | from qupulse.program.loop import Loop 13 | from qupulse.hardware.util import voltage_to_uint16 14 | 15 | from tests.pulses.sequencing_dummies import DummyWaveform 16 | from tests._program.loop_tests import LoopTests, WaveformGenerator 17 | 18 | 19 | class ConfigurationGuardTest(unittest.TestCase): 20 | class DummyChannelPair: 21 | def __init__(self, test_obj: unittest.TestCase): 22 | self.test_obj = test_obj 23 | self._configuration_guard_count = 0 24 | self.is_in_config_mode = False 25 | 26 | def _enter_config_mode(self): 27 | self.test_obj.assertFalse(self.is_in_config_mode) 28 | self.test_obj.assertEqual(self._configuration_guard_count, 0) 29 | self.is_in_config_mode = True 30 | 31 | def _exit_config_mode(self): 32 | self.test_obj.assertTrue(self.is_in_config_mode) 33 | self.test_obj.assertEqual(self._configuration_guard_count, 0) 34 | self.is_in_config_mode = False 35 | 36 | @with_configuration_guard 37 | def guarded_method(self, counter=5, throw=False): 38 | self.test_obj.assertTrue(self.is_in_config_mode) 39 | if counter > 0: 40 | return self.guarded_method(counter - 1, throw) + 1 41 | if throw: 42 | raise RuntimeError() 43 | return 0 44 | 45 | def test_config_guard(self): 46 | channel_pair = ConfigurationGuardTest.DummyChannelPair(self) 47 | 48 | for i in range(5): 49 | self.assertEqual(channel_pair.guarded_method(i), i) 50 | 51 | with self.assertRaises(RuntimeError): 52 | channel_pair.guarded_method(1, True) 53 | 54 | self.assertFalse(channel_pair.is_in_config_mode) 55 | 56 | 57 | -------------------------------------------------------------------------------- /tests/hardware/util_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | try: 6 | import zhinst.utils 7 | except ImportError: 8 | zhinst = None 9 | 10 | from qupulse.utils.types import TimeType 11 | from qupulse.hardware.util import voltage_to_uint16, find_positions, get_sample_times, not_none_indices, \ 12 | zhinst_voltage_to_uint16 13 | from tests.pulses.sequencing_dummies import DummyWaveform 14 | 15 | 16 | class VoltageToBinaryTests(unittest.TestCase): 17 | 18 | def test_voltage_to_uint16(self): 19 | 20 | with self.assertRaises(ValueError): 21 | voltage_to_uint16(np.zeros(0), 0, 0, 0) 22 | 23 | linspace_voltage = np.linspace(0, 1, 128) 24 | with self.assertRaises(ValueError): 25 | voltage_to_uint16(linspace_voltage, 0.9, 0, 1) 26 | 27 | with self.assertRaises(ValueError): 28 | voltage_to_uint16(linspace_voltage, 1.1, -1, 1) 29 | 30 | expected_data = np.arange(0, 128, dtype=np.uint16) 31 | received_data = voltage_to_uint16(linspace_voltage, 0.5, 0.5, 7) 32 | 33 | self.assertTrue(np.all(expected_data == received_data)) 34 | 35 | def test_zero_level_14bit(self): 36 | zero_level = voltage_to_uint16(np.zeros(1), 0.5, 0., 14) 37 | self.assertEqual(zero_level, 8192) 38 | 39 | 40 | class FindPositionTest(unittest.TestCase): 41 | def test_find_position(self): 42 | data = [2, 6, -24, 65, 46, 5, -10, 9] 43 | to_find = [54, 12, 5, -10, 45, 6, 2] 44 | 45 | positions = find_positions(data, to_find) 46 | 47 | self.assertEqual(positions.tolist(), [-1, -1, 5, 6, -1, 1, 0]) 48 | 49 | 50 | class SampleTimeCalculationTest(unittest.TestCase): 51 | def test_get_sample_times(self): 52 | sample_rate = TimeType.from_fraction(12, 10) 53 | wf1 = DummyWaveform(duration=TimeType.from_fraction(20, 12)) 54 | wf2 = DummyWaveform(duration=TimeType.from_fraction(400000000001, 120000000000)) 55 | wf3 = DummyWaveform(duration=TimeType.from_fraction(1, 10**15)) 56 | 57 | expected_times = np.arange(4) / 1.2 58 | times, n_samples = get_sample_times([wf1, wf2], sample_rate_in_GHz=sample_rate) 59 | np.testing.assert_equal(expected_times, times) 60 | np.testing.assert_equal(n_samples, np.asarray([2, 4])) 61 | 62 | with self.assertRaises(AssertionError): 63 | get_sample_times([], sample_rate_in_GHz=sample_rate) 64 | 65 | with self.assertRaisesRegex(ValueError, "non integer length"): 66 | get_sample_times([wf1, wf2], sample_rate_in_GHz=sample_rate, tolerance=0.) 67 | 68 | with self.assertRaisesRegex(ValueError, "length <= zero"): 69 | get_sample_times([wf1, wf3], sample_rate_in_GHz=sample_rate) 70 | 71 | def test_get_sample_times_single_wf(self): 72 | sample_rate = TimeType.from_fraction(12, 10) 73 | wf = DummyWaveform(duration=TimeType.from_fraction(40, 12)) 74 | 75 | expected_times = np.arange(4) / 1.2 76 | times, n_samples = get_sample_times(wf, sample_rate_in_GHz=sample_rate) 77 | 78 | np.testing.assert_equal(times, expected_times) 79 | np.testing.assert_equal(n_samples, np.asarray(4)) 80 | 81 | 82 | class NotNoneIndexTest(unittest.TestCase): 83 | def test_not_none_indices(self): 84 | self.assertEqual(([None, 0, 1, None, None, 2], 3), 85 | not_none_indices([None, 'a', 'b', None, None, 'c'])) 86 | 87 | 88 | @unittest.skipIf(zhinst is None, "zhinst not installed") 89 | class ZHInstVoltageToUint16Test(unittest.TestCase): 90 | def test_size_exception(self): 91 | with self.assertRaisesRegex(ValueError, "No input"): 92 | zhinst_voltage_to_uint16(None, None, (None, None, None, None)) 93 | with self.assertRaisesRegex(ValueError, "dimension"): 94 | zhinst_voltage_to_uint16(np.zeros(192), np.zeros(191), (None, None, None, None)) 95 | with self.assertRaisesRegex(ValueError, "dimension"): 96 | zhinst_voltage_to_uint16(np.zeros(192), None, (np.zeros(191), None, None, None)) 97 | 98 | def test_range_exception(self): 99 | with self.assertRaisesRegex(ValueError, "invalid"): 100 | zhinst_voltage_to_uint16(2.*np.ones(192), None, (None, None, None, None)) 101 | # this should work 102 | zhinst_voltage_to_uint16(None, None, (2. * np.ones(192), None, None, None)) 103 | 104 | def test_zeros(self): 105 | combined = zhinst_voltage_to_uint16(None, np.zeros(192), (None, None, None, None)) 106 | np.testing.assert_array_equal(np.zeros(3*192, dtype=np.uint16), combined) 107 | 108 | def test_full(self): 109 | ch1 = np.linspace(0, 1., num=192) 110 | ch2 = np.linspace(0., -1., num=192) 111 | 112 | markers = tuple(np.array(([1.] + [0.]*m) * 192)[:192] for m in range(1, 5)) 113 | 114 | combined = zhinst_voltage_to_uint16(ch1, ch2, markers) 115 | 116 | marker_data = [sum(int(markers[m][idx] > 0) << m for m in range(4)) 117 | for idx in range(192)] 118 | marker_data = np.array(marker_data, dtype=np.uint16) 119 | expected = zhinst.utils.convert_awg_waveform(ch1, ch2, marker_data) 120 | 121 | np.testing.assert_array_equal(expected, combined) 122 | -------------------------------------------------------------------------------- /tests/pulses/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qutech/qupulse/15d67075362c3a056b942f8feb0818f48a5cc1cb/tests/pulses/__init__.py -------------------------------------------------------------------------------- /tests/pulses/bug_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | import numpy as np 5 | 6 | from qupulse.pulses.table_pulse_template import TablePulseTemplate 7 | from qupulse.pulses.function_pulse_template import FunctionPulseTemplate 8 | from qupulse.pulses.sequence_pulse_template import SequencePulseTemplate 9 | from qupulse.pulses.repetition_pulse_template import RepetitionPulseTemplate 10 | from qupulse.pulses.multi_channel_pulse_template import AtomicMultiChannelPulseTemplate 11 | from qupulse.pulses.mapping_pulse_template import MappingPulseTemplate 12 | from qupulse.pulses.loop_pulse_template import ForLoopPulseTemplate 13 | 14 | from qupulse.plotting import plot 15 | 16 | from qupulse.program.loop import to_waveform 17 | from qupulse.utils import isclose 18 | 19 | class BugTests(unittest.TestCase): 20 | 21 | def test_plotting_two_channel_function_pulse_after_two_channel_table_pulse_crash(self) -> None: 22 | """ successful if no crash -> no asserts """ 23 | template = TablePulseTemplate(entries={'A': [(0, 0), 24 | ('ta', 'va', 'hold'), 25 | ('tb', 'vb', 'linear'), 26 | ('tend', 0, 'jump')], 27 | 'B': [(0, 0), 28 | ('ta', '-va', 'hold'), 29 | ('tb', '-vb', 'linear'), 30 | ('tend', 0, 'jump')]}, measurements=[('m', 0, 'ta'), 31 | ('n', 'tb', 'tend-tb')]) 32 | 33 | parameters = {'ta': 2, 34 | 'va': 2, 35 | 'tb': 4, 36 | 'vb': 3, 37 | 'tc': 5, 38 | 'td': 11, 39 | 'tend': 6} 40 | _ = plot(template, parameters, sample_rate=100, show=False, plot_measurements={'m', 'n'}) 41 | 42 | repeated_template = RepetitionPulseTemplate(template, 'n_rep') 43 | sine_template = FunctionPulseTemplate('sin_a*sin(t)', '2*3.1415') 44 | two_channel_sine_template = AtomicMultiChannelPulseTemplate( 45 | (sine_template, {'default': 'A'}), 46 | (sine_template, {'default': 'B'}, {'sin_a': 'sin_b'}) 47 | ) 48 | sequence_template = SequencePulseTemplate(repeated_template, two_channel_sine_template) 49 | #sequence_template = SequencePulseTemplate(two_channel_sine_template, repeated_template) # this was working fine 50 | 51 | sequence_parameters = dict(parameters) # we just copy our parameter dict from before 52 | sequence_parameters['n_rep'] = 4 # and add a few new values for the new params from the sine wave 53 | sequence_parameters['sin_a'] = 1 54 | sequence_parameters['sin_b'] = 2 55 | 56 | _ = plot(sequence_template, parameters=sequence_parameters, sample_rate=100, show=False) 57 | 58 | def test_plot_with_parameter_value_being_expression_string(self) -> None: 59 | sine_measurements = [('M', 't_duration/2', 't_duration')] 60 | sine = FunctionPulseTemplate('a*sin(omega*t)', 't_duration', measurements=sine_measurements) 61 | sine_channel_mapping = dict(default='sin_channel') 62 | sine_measurement_mapping = dict(M='M_sin') 63 | remapped_sine = MappingPulseTemplate(sine, measurement_mapping=sine_measurement_mapping, 64 | channel_mapping=sine_channel_mapping) 65 | cos_measurements = [('M', 0, 't_duration/2')] 66 | cos = FunctionPulseTemplate('a*cos(omega*t)', 't_duration', measurements=cos_measurements) 67 | cos_channel_mapping = dict(default='cos_channel') 68 | cos_measurement_mapping = dict(M='M_cos') 69 | remapped_cos = MappingPulseTemplate(cos, channel_mapping=cos_channel_mapping, measurement_mapping=cos_measurement_mapping) 70 | both = AtomicMultiChannelPulseTemplate(remapped_sine, remapped_cos) 71 | 72 | parameter_values = dict(omega=1.0, a=1.0, t_duration="2*pi") 73 | 74 | _ = plot(both, parameters=parameter_values, sample_rate=100) 75 | 76 | def test_issue_584_uninitialized_table_sample(self): 77 | """issue 584""" 78 | d = 598.3333333333334 - 480 79 | tpt = TablePulseTemplate(entries={'P': [(0, 1.0, 'hold'), (d, 1.0, 'hold')]}) 80 | with mock.patch('qupulse.program.waveforms.PULSE_TO_WAVEFORM_ERROR', 1e-6): 81 | wf = to_waveform(tpt.create_program()) 82 | self.assertTrue(isclose(d, wf.duration, abs_tol=1e-6)) 83 | 84 | start_time = 0. 85 | end_time = wf.duration 86 | sample_rate = 3. 87 | 88 | sample_count = (end_time - start_time) * sample_rate + 1 89 | 90 | times = np.linspace(float(start_time), float(wf.duration), num=int(sample_count), dtype=float) 91 | times[-1] = np.nextafter(times[-1], times[-2]) 92 | 93 | out = np.full_like(times, fill_value=np.nan) 94 | sampled = wf.get_sampled(channel='P', sample_times=times, output_array=out) 95 | 96 | expected = np.full_like(times, fill_value=1.) 97 | np.testing.assert_array_equal(expected, sampled) 98 | 99 | def test_issue_612_for_loop_duration(self): 100 | fpt = FunctionPulseTemplate('sin(2*pi*i*t*f)', '1/f') 101 | pt = ForLoopPulseTemplate(fpt, 'i', 'floor(total_time*f)') 102 | self.assertEqual( 103 | (500 + 501) // 2, 104 | pt.duration.evaluate_in_scope({'f': 1., 'total_time': 500}) 105 | ) 106 | 107 | -------------------------------------------------------------------------------- /tests/pulses/interpolation_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | 4 | from qupulse.pulses.interpolation import LinearInterpolationStrategy, HoldInterpolationStrategy, JumpInterpolationStrategy 5 | 6 | 7 | class InterpolationTest(unittest.TestCase): 8 | 9 | def test_linear_interpolation(self): 10 | start = (-1, -1) 11 | end = (3,3) 12 | t = np.arange(-1, 4, dtype=float) 13 | strat = LinearInterpolationStrategy() 14 | result = strat(start, end, t) 15 | self.assertTrue(all(t == result)) 16 | # TODO: Discussion: May start > end? 17 | 18 | def test_hold_interpolation(self): 19 | start = (-1, -1) 20 | end = (3,3) 21 | t = np.linspace(-1,3,100) 22 | strat = HoldInterpolationStrategy() 23 | result = strat(start, end, t) 24 | self.assertTrue(all(result == -1)) 25 | with self.assertRaises(ValueError): 26 | strat(end, start, t) 27 | 28 | def test_jump_interpolation(self): 29 | start = (-1, -1) 30 | end = (3,3) 31 | t = np.linspace(-1,3,100) 32 | strat = JumpInterpolationStrategy() 33 | result = strat(start, end, t) 34 | self.assertTrue(all(result == 3)) 35 | with self.assertRaises(ValueError): 36 | strat(end, start, t) 37 | 38 | def test_repr_str(self): 39 | #Test hash 40 | strategies = {LinearInterpolationStrategy():("linear",""), 41 | HoldInterpolationStrategy(): ("hold", ""), 42 | JumpInterpolationStrategy(): ("jump", "")} 43 | 44 | for strategy in strategies: 45 | repr_ = strategies[strategy][1] 46 | str_ = strategies[strategy][0] 47 | self.assertEqual(repr(strategy), repr_) 48 | self.assertEqual(str(strategy), str_) 49 | self.assertTrue(LinearInterpolationStrategy()!=HoldInterpolationStrategy()) 50 | self.assertTrue(LinearInterpolationStrategy()!=JumpInterpolationStrategy()) 51 | self.assertTrue(JumpInterpolationStrategy()!=HoldInterpolationStrategy()) 52 | 53 | 54 | 55 | 56 | if __name__ == "__main__": 57 | unittest.main(verbosity=2) -------------------------------------------------------------------------------- /tests/pulses/parameters_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from qupulse.expressions import Expression 4 | from qupulse.pulses.parameters import ParameterNotProvidedException,\ 5 | ParameterConstraint, InvalidParameterNameException 6 | 7 | 8 | class ParameterConstraintTest(unittest.TestCase): 9 | def test_ordering(self): 10 | constraint = ParameterConstraint('a <= b') 11 | self.assertEqual(constraint.affected_parameters, {'a', 'b'}) 12 | 13 | self.assertTrue(constraint.is_fulfilled(dict(a=1, b=2))) 14 | self.assertTrue(constraint.is_fulfilled(dict(a=2, b=2))) 15 | self.assertFalse(constraint.is_fulfilled(dict(a=2, b=1))) 16 | 17 | def test_equal(self): 18 | constraint = ParameterConstraint('a==b') 19 | self.assertEqual(constraint.affected_parameters, {'a', 'b'}) 20 | 21 | self.assertTrue(constraint.is_fulfilled(dict(a=2, b=2))) 22 | self.assertFalse(constraint.is_fulfilled(dict(a=3, b=2))) 23 | 24 | def test_expressions(self): 25 | constraint = ParameterConstraint('Max(a, b) < a*c') 26 | self.assertEqual(constraint.affected_parameters, {'a', 'b', 'c'}) 27 | 28 | self.assertTrue(constraint.is_fulfilled(dict(a=2, b=2, c=3))) 29 | self.assertFalse(constraint.is_fulfilled(dict(a=3, b=5, c=1))) 30 | 31 | def test_no_relation(self): 32 | with self.assertRaises(ValueError): 33 | ParameterConstraint('a*b') 34 | ParameterConstraint('1 < 2') 35 | 36 | def test_str_and_serialization(self): 37 | self.assertEqual(str(ParameterConstraint('a < b')), 'a < b') 38 | self.assertEqual(ParameterConstraint('a < b').get_serialization_data(), 'a < b') 39 | 40 | self.assertEqual(str(ParameterConstraint('a==b')), 'a==b') 41 | self.assertEqual(ParameterConstraint('a==b').get_serialization_data(), 'a==b') 42 | 43 | def test_repr(self): 44 | pc = ParameterConstraint('a < b') 45 | self.assertEqual("ParameterConstraint('a < b')", repr(pc)) 46 | 47 | 48 | class ParameterNotProvidedExceptionTests(unittest.TestCase): 49 | 50 | def test(self) -> None: 51 | exc = ParameterNotProvidedException('foo') 52 | self.assertEqual("No value was provided for parameter 'foo'.", str(exc)) 53 | 54 | 55 | class InvalidParameterNameExceptionTests(unittest.TestCase): 56 | def test(self): 57 | exception = InvalidParameterNameException('asd') 58 | 59 | self.assertEqual(exception.parameter_name, 'asd') 60 | self.assertEqual(str(exception), 'asd is an invalid parameter name') 61 | 62 | 63 | if __name__ == "__main__": 64 | unittest.main(verbosity=2) 65 | 66 | -------------------------------------------------------------------------------- /tests/pulses/pulse_template_parameter_mapping_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import warnings 3 | 4 | from qupulse.serialization import Serializer 5 | from tests.pulses.sequencing_dummies import DummyPulseTemplate 6 | from tests.serialization_dummies import DummyStorageBackend 7 | 8 | 9 | class TestPulseTemplateParameterMappingFileTests(unittest.TestCase): 10 | 11 | # ensure that a MappingPulseTemplate imported from pulse_template_parameter_mapping serializes as from mapping_pulse_template 12 | def test_pulse_template_parameter_include(self) -> None: 13 | with warnings.catch_warnings(record=True): 14 | warnings.simplefilter('ignore', DeprecationWarning) 15 | from qupulse.pulses.pulse_template_parameter_mapping import MappingPulseTemplate 16 | dummy_t = DummyPulseTemplate() 17 | map_t = MappingPulseTemplate(dummy_t) 18 | type_str = map_t.get_type_identifier() 19 | self.assertEqual("qupulse.pulses.mapping_pulse_template.MappingPulseTemplate", type_str) 20 | 21 | -------------------------------------------------------------------------------- /tests/pulses/time_reversal_pulse_template_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | from qupulse.pulses import ConstantPT, FunctionPT 6 | from qupulse.plotting import render 7 | from qupulse.pulses.time_reversal_pulse_template import TimeReversalPulseTemplate 8 | from qupulse.utils.types import TimeType 9 | from qupulse.expressions import ExpressionScalar 10 | from qupulse.program.loop import LoopBuilder 11 | from qupulse.program.linspace import LinSpaceBuilder, LinSpaceVM, to_increment_commands 12 | from tests.pulses.sequencing_dummies import DummyPulseTemplate 13 | from tests.serialization_tests import SerializableTests 14 | from tests.program.linspace_tests import assert_vm_output_almost_equal 15 | 16 | class TimeReversalPulseTemplateTests(unittest.TestCase): 17 | def test_simple_properties(self): 18 | inner = DummyPulseTemplate(identifier='d', 19 | defined_channels={'A', 'B'}, 20 | duration=ExpressionScalar(42), 21 | integrals={'A': ExpressionScalar(4), 'B': ExpressionScalar('alpha')}, 22 | parameter_names={'alpha', 'beta'}) 23 | 24 | reversed_pt = TimeReversalPulseTemplate(inner, identifier='reverse') 25 | 26 | self.assertEqual(reversed_pt.duration, inner.duration) 27 | self.assertEqual(reversed_pt.parameter_names, inner.parameter_names) 28 | self.assertEqual(reversed_pt.integral, inner.integral) 29 | self.assertEqual(reversed_pt.defined_channels, inner.defined_channels) 30 | 31 | self.assertEqual(reversed_pt.identifier, 'reverse') 32 | 33 | def test_time_reversal_loop(self): 34 | inner = ConstantPT(4, {'a': 3}) @ FunctionPT('sin(t)', 5, channel='a') 35 | manual_reverse = FunctionPT('sin(5 - t)', 5, channel='a') @ ConstantPT(4, {'a': 3}) 36 | time_reversed = TimeReversalPulseTemplate(inner) 37 | 38 | program = time_reversed.create_program(program_builder=LoopBuilder()) 39 | manual_program = manual_reverse.create_program(program_builder=LoopBuilder()) 40 | 41 | t, data, _ = render(program, 9 / 10) 42 | _, manual_data, _ = render(manual_program, 9 / 10) 43 | 44 | np.testing.assert_allclose(data['a'], manual_data['a']) 45 | 46 | def test_time_reversal_linspace(self): 47 | constant_pt = ConstantPT(4, {'a': '3.0 + x * 1.0 + y * -0.3'}) 48 | function_pt = FunctionPT('sin(t)', 5, channel='a') 49 | reversed_function_pt = function_pt.with_time_reversal() 50 | 51 | inner = (constant_pt @ function_pt).with_iteration('x', 6) 52 | inner_manual = (reversed_function_pt @ constant_pt).with_iteration('x', (5, -1, -1)) 53 | 54 | outer = inner.with_time_reversal().with_iteration('y', 8) 55 | outer_man = inner_manual.with_iteration('y', 8) 56 | 57 | self.assertEqual(outer.duration, outer_man.duration) 58 | 59 | program = outer.create_program(program_builder=LinSpaceBuilder(channels=('a',))) 60 | manual_program = outer_man.create_program(program_builder=LinSpaceBuilder(channels=('a',))) 61 | 62 | commands = to_increment_commands(program) 63 | manual_commands = to_increment_commands(manual_program) 64 | self.assertEqual(commands, manual_commands) 65 | 66 | manual_vm = LinSpaceVM(1) 67 | manual_vm.set_commands(manual_commands) 68 | manual_vm.run() 69 | 70 | vm = LinSpaceVM(1) 71 | vm.set_commands(commands) 72 | vm.run() 73 | 74 | assert_vm_output_almost_equal(self, manual_vm.history, vm.history) 75 | 76 | 77 | class TimeReversalPulseTemplateSerializationTests(unittest.TestCase, SerializableTests): 78 | @property 79 | def class_to_test(self): 80 | return TimeReversalPulseTemplate 81 | 82 | def make_kwargs(self) -> dict: 83 | return dict( 84 | inner=DummyPulseTemplate(identifier='d', 85 | defined_channels={'A', 'B'}, 86 | duration=ExpressionScalar(42), 87 | integrals={'A': ExpressionScalar(4), 'B': ExpressionScalar('alpha')}, 88 | parameter_names={'alpha', 'beta'}), 89 | ) 90 | 91 | def assert_equal_instance_except_id(self, lhs, rhs): 92 | return lhs._inner == rhs._inner 93 | 94 | -------------------------------------------------------------------------------- /tests/serialization_dummies.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Dict, Any, Callable, Iterator 2 | 3 | from qupulse.serialization import Serializer, Serializable, StorageBackend 4 | 5 | 6 | class DummyStorageBackend(StorageBackend): 7 | 8 | def __init__(self) -> None: 9 | self.stored_items = dict() 10 | self.times_get_called = 0 11 | self.times_put_called = 0 12 | self.times_exists_called = 0 13 | 14 | def get(self, identifier: str) -> str: 15 | self.times_get_called += 1 16 | if identifier not in self.stored_items: 17 | raise KeyError(identifier) 18 | return self.stored_items[identifier] 19 | 20 | def put(self, identifier: str, data: str, overwrite: bool=False) -> None: 21 | self.times_put_called += 1 22 | if identifier in self.stored_items and not overwrite: 23 | raise FileExistsError() 24 | self.stored_items[identifier] = data 25 | 26 | def exists(self, identifier: str) -> bool: 27 | self.times_exists_called += 1 28 | return identifier in self.stored_items 29 | 30 | def delete(self, identifier: str) -> None: 31 | del self.stored_items[identifier] 32 | 33 | def __iter__(self) -> Iterator[str]: 34 | return iter(self.stored_items) 35 | 36 | 37 | class DummySerializer(Serializer): 38 | 39 | def __init__(self, 40 | serialize_callback: Callable[[Serializable], str] = lambda x: "{}".format(id(x)), 41 | identifier_callback: Callable[[Serializable], str] = lambda x: "{}".format(id(x)), 42 | deserialize_callback: Callable[[Any], str] = lambda x: x) -> None: 43 | self.backend = DummyStorageBackend() 44 | self.serialize_callback = serialize_callback 45 | self.identifier_callback = identifier_callback 46 | self.deserialize_callback = deserialize_callback 47 | super().__init__(self.backend) 48 | self.subelements = dict() 49 | 50 | def dictify(self, serializable: Serializable) -> None: 51 | identifier = self.identifier_callback(serializable) 52 | self.subelements[identifier] = serializable 53 | return self.serialize_callback(serializable) 54 | 55 | def deserialize(self, representation: Union[str, Dict[str, Any]]) -> Serializable: 56 | return self.subelements[self.deserialize_callback(representation)] 57 | 58 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qutech/qupulse/15d67075362c3a056b942f8feb0818f48a5cc1cb/tests/utils/__init__.py -------------------------------------------------------------------------------- /tests/utils/numeric_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from typing import Callable, List, Iterator, Tuple 3 | import random 4 | from fractions import Fraction 5 | from collections import deque 6 | from itertools import islice 7 | 8 | from qupulse.utils.numeric import approximate_rational, approximate_double, smallest_factor_ge, lcm 9 | 10 | 11 | def stern_brocot_sequence() -> Iterator[int]: 12 | sb = deque([1, 1]) 13 | while True: 14 | sb += [sb[0] + sb[1], sb[1]] 15 | yield sb.popleft() 16 | 17 | 18 | def stern_brocot_tree(depth: int) -> List[Fraction]: 19 | """see wikipedia article""" 20 | fractions = [Fraction(0), Fraction(1)] 21 | 22 | seq = stern_brocot_sequence() 23 | next(seq) 24 | 25 | for n in range(depth): 26 | for _ in range(n + 1): 27 | p = next(seq) 28 | q = next(seq) 29 | fractions.append(Fraction(p, q)) 30 | 31 | return sorted(fractions) 32 | 33 | 34 | def window(iterable, n): 35 | assert n > 0 36 | it = iter(iterable) 37 | state = deque(islice(it, 0, n - 1), maxlen=n) 38 | for new_element in it: 39 | state.append(new_element) 40 | yield tuple(state) 41 | state.popleft() 42 | 43 | 44 | def uniform_without_bounds(rng, a, b): 45 | result = a 46 | while not a < result < b: 47 | result = rng.uniform(a, b) 48 | return result 49 | 50 | 51 | def generate_test_pairs(depth: int, seed) -> Iterator[Tuple[Tuple[Fraction, Fraction], Fraction]]: 52 | rng = random.Random(seed) 53 | tree = stern_brocot_tree(depth) 54 | extended_tree = [float('-inf')] + tree + [float('inf')] 55 | 56 | # values map to themselves 57 | for a, b, c in window(tree, 3): 58 | err = min(b - a, c - b) 59 | yield (b, err), b 60 | 61 | for prev, a, b, upcom in zip(extended_tree, extended_tree[1:], extended_tree[2:], extended_tree[3:]): 62 | mid = (a + b) / 2 63 | 64 | low = Fraction(uniform_without_bounds(rng, a, mid)) 65 | err = min(mid - a, low - prev) 66 | yield (low, err), a 67 | 68 | high = Fraction(uniform_without_bounds(rng, mid, b)) 69 | err = min(b - mid, upcom - high) 70 | yield (high, err), b 71 | 72 | 73 | class ApproximationTests(unittest.TestCase): 74 | def test_approximate_rational(self): 75 | """Use Stern-Brocot tree and rng to generate test cases where we know the result""" 76 | depth = 70 # equivalent to 7457 test cases 77 | test_pairs = list(generate_test_pairs(depth, seed=42)) 78 | 79 | for offset in (-2, -1, 0, 1, 2): 80 | for (x, abs_err), result in test_pairs: 81 | expected = result + offset 82 | result = approximate_rational(x + offset, abs_err, Fraction) 83 | self.assertEqual(expected, result) 84 | 85 | with self.assertRaises(ValueError): 86 | approximate_rational(Fraction(3, 1), Fraction(0, 100), Fraction) 87 | 88 | with self.assertRaises(ValueError): 89 | approximate_rational(Fraction(3, 1), Fraction(-1, 100), Fraction) 90 | 91 | x = Fraction(3, 1) 92 | abs_err = Fraction(1, 100) 93 | self.assertIs(x, approximate_rational(x, abs_err, Fraction)) 94 | 95 | def test_approximate_double(self): 96 | test_values = [ 97 | ((.1, .05), Fraction(1, 7)), 98 | ((.12, .005), Fraction(2, 17)), 99 | ((.15, .005), Fraction(2, 13)), 100 | # .111_111_12, 0.000_000_005 101 | ((.11111112, 0.000000005), Fraction(888890, 8000009)), 102 | ((.111125, 0.0000005), Fraction(859, 7730)), 103 | ((2.50000000008, .1), Fraction(5, 2)) 104 | ] 105 | 106 | for (x, err), expected in test_values: 107 | result = approximate_double(x, err, Fraction) 108 | self.assertEqual(expected, result, msg='{x} ± {err} results in {result} ' 109 | 'which is not the expected {expected}'.format(x=x, err=err, 110 | result=result, 111 | expected=expected)) 112 | 113 | 114 | class FactorizationTests(unittest.TestCase): 115 | def test_smallest_factor_ge(self): 116 | for brute_force in range(100): 117 | self.assertEqual(smallest_factor_ge(15, 2), 3, brute_force) 118 | self.assertEqual(smallest_factor_ge(15, 4), 5, brute_force) 119 | self.assertEqual(smallest_factor_ge(45, 2), 3, brute_force) 120 | self.assertEqual(smallest_factor_ge(45, 4), 5, brute_force) 121 | self.assertEqual(smallest_factor_ge(45, 5), 5, brute_force) 122 | self.assertEqual(smallest_factor_ge(36, 8), 9, brute_force) 123 | 124 | 125 | class LeastCommonMultipleTests(unittest.TestCase): 126 | def test_few_args(self): 127 | self.assertEqual(1, lcm()) 128 | self.assertEqual(5, lcm(5)) 129 | 130 | def test_multi_args(self): 131 | self.assertEqual(15, lcm(3, 5)) 132 | self.assertEqual(0, lcm(3, 0)) 133 | self.assertEqual(20, lcm(2, 5, 4, 10)) 134 | -------------------------------------------------------------------------------- /tests/utils/performance_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import warnings 3 | 4 | import numpy as np 5 | 6 | from qupulse.utils.performance import ( 7 | _time_windows_to_samples_numba, _time_windows_to_samples_numpy, 8 | _average_windows_numba, _average_windows_numpy, average_windows, 9 | shrink_overlapping_windows, WindowOverlapWarning) 10 | 11 | 12 | class TimeWindowsToSamplesTest(unittest.TestCase): 13 | @staticmethod 14 | def assert_implementations_equal(begins, lengths, sample_rate): 15 | np.testing.assert_equal( 16 | _time_windows_to_samples_numba(begins, lengths, sample_rate), 17 | _time_windows_to_samples_numpy(begins, lengths, sample_rate) 18 | ) 19 | 20 | def test_monotonic(self): 21 | begins = np.array([101.3, 123.6218764354, 176.31, 763454.776]) 22 | lengths = np.array([6.4234, 24.8654413, 8765.45, 12543.]) 23 | 24 | for sr in (0.1, 1/9, 1., 2.764423123563463412342, 100.322): 25 | self.assert_implementations_equal(begins, lengths, sr) 26 | 27 | def test_unsorted(self): 28 | begins = np.array([101.3, 176.31, 763454.776, 123.6218764354]) 29 | lengths = np.array([6.4234, 8765.45, 12543., 24.8654413]) 30 | 31 | for sr in (0.1, 1/9, 1., 2.764423123563463412342, 100.322): 32 | self.assert_implementations_equal(begins, lengths, sr) 33 | 34 | 35 | class WindowAverageTest(unittest.TestCase): 36 | @staticmethod 37 | def assert_implementations_equal(time, values, begins, ends): 38 | numpy_result = _average_windows_numpy(time, values, begins, ends) 39 | numba_result = _average_windows_numba(time, values, begins, ends) 40 | np.testing.assert_allclose(numpy_result, numba_result) 41 | 42 | def setUp(self): 43 | self.begins = np.array([1., 2., 3.] + [4.] + [6., 7., 8., 9., 10.]) 44 | self.ends = self.begins + np.array([1., 1., 1.] + [3.] + [2., 2., 2., 2., 2.]) 45 | self.time = np.arange(10).astype(float) 46 | self.values = np.asarray([ 47 | np.sin(self.time), 48 | np.cos(self.time), 49 | ]).T 50 | 51 | def test_dispatch(self): 52 | _ = average_windows(self.time, self.values, self.begins, self.ends) 53 | _ = average_windows(self.time, self.values[..., 0], self.begins, self.ends) 54 | 55 | def test_single_channel(self): 56 | self.assert_implementations_equal(self.time, self.values[..., 0], self.begins, self.ends) 57 | self.assert_implementations_equal(self.time, self.values[..., :1], self.begins, self.ends) 58 | 59 | def test_dual_channel(self): 60 | self.assert_implementations_equal(self.time, self.values, self.begins, self.ends) 61 | 62 | 63 | class TestOverlappingWindowReduction(unittest.TestCase): 64 | def setUp(self): 65 | self.shrank = np.array([1, 4, 8], dtype=np.uint64), np.array([3, 4, 4], dtype=np.uint64) 66 | self.to_shrink = np.array([1, 4, 7], dtype=np.uint64), np.array([3, 4, 5], dtype=np.uint64) 67 | 68 | def assert_noop(self, shrink_fn): 69 | begins = np.array([1, 3, 5], dtype=np.uint64) 70 | lengths = np.array([2, 1, 6], dtype=np.uint64) 71 | result = shrink_fn(begins, lengths) 72 | np.testing.assert_equal((begins, lengths), result) 73 | 74 | begins = (np.arange(100) * 176.5).astype(dtype=np.uint64) 75 | lengths = (np.ones(100) * 10 * np.pi).astype(dtype=np.uint64) 76 | result = shrink_fn(begins, lengths) 77 | np.testing.assert_equal((begins, lengths), result) 78 | 79 | begins = np.arange(15, dtype=np.uint64)*16 80 | lengths = 1+np.arange(15, dtype=np.uint64) 81 | result = shrink_fn(begins, lengths) 82 | np.testing.assert_equal((begins, lengths), result) 83 | 84 | def assert_shrinks(self, shrink_fn): 85 | with warnings.catch_warnings(): 86 | warnings.simplefilter("always", WindowOverlapWarning) 87 | with self.assertWarns(WindowOverlapWarning): 88 | shrank = shrink_fn(*self.to_shrink) 89 | np.testing.assert_equal(self.shrank, shrank) 90 | 91 | def assert_empty_window_error(self, shrink_fn): 92 | invalid = np.array([1, 2], dtype=np.uint64), np.array([5, 1], dtype=np.uint64) 93 | with self.assertRaisesRegex(ValueError, "Overlap is bigger than measurement window"): 94 | shrink_fn(*invalid) 95 | 96 | def test_shrink_overlapping_windows_numba(self): 97 | def shrink_fn(begins, lengths): 98 | return shrink_overlapping_windows(begins, lengths, use_numba=True) 99 | 100 | self.assert_noop(shrink_fn) 101 | self.assert_shrinks(shrink_fn) 102 | self.assert_empty_window_error(shrink_fn) 103 | 104 | def test_shrink_overlapping_windows_numpy(self): 105 | def shrink_fn(begins, lengths): 106 | return shrink_overlapping_windows(begins, lengths, use_numba=False) 107 | 108 | self.assert_noop(shrink_fn) 109 | self.assert_shrinks(shrink_fn) 110 | self.assert_empty_window_error(shrink_fn) 111 | -------------------------------------------------------------------------------- /tests/utils/tree_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import weakref 3 | 4 | from qupulse.utils.tree import Node 5 | 6 | 7 | class SpecialNode(Node): 8 | def __init__(self, my_argument=None, **kwargs): 9 | super().__init__(**kwargs) 10 | self.init_arg = my_argument 11 | 12 | 13 | class NodeTests(unittest.TestCase): 14 | def __init__(self, *args, **kwargs): 15 | super().__init__(*args, **kwargs) 16 | 17 | def test_init(self): 18 | 19 | children = [Node(parent=None, children=[]) for _ in range(5)] 20 | 21 | root = Node(parent=None, children=children) 22 | 23 | for c1, c2 in zip(children, root): 24 | self.assertIs(c1, c2) 25 | self.assertIs(c1.parent, root) 26 | 27 | def test_parse_children(self): 28 | 29 | root = Node() 30 | to_parse = Node() 31 | parsed = root.parse_child(to_parse) 32 | self.assertIs(parsed.parent, root) 33 | # maybe change this behaviour? 34 | self.assertIs(parsed, to_parse) 35 | 36 | sub_node = Node() 37 | to_parse = dict(children=[sub_node]) 38 | parsed = root.parse_child(to_parse) 39 | self.assertIs(parsed.parent, root) 40 | self.assertIs(parsed.children[0], sub_node) 41 | with self.assertRaises(TypeError): 42 | root.parse_child(SpecialNode()) 43 | 44 | def test_parse_children_derived(self): 45 | root = SpecialNode() 46 | to_parse = SpecialNode() 47 | parsed = root.parse_child(to_parse) 48 | self.assertIs(parsed.parent, root) 49 | # maybe change this behaviour? 50 | self.assertIs(parsed, to_parse) 51 | 52 | sub_node = SpecialNode() 53 | to_parse = dict(children=[sub_node], my_argument=6) 54 | parsed = root.parse_child(to_parse) 55 | self.assertIs(parsed.parent, root) 56 | self.assertEqual(parsed.init_arg, 6) 57 | with self.assertRaises(TypeError): 58 | root.parse_child(Node()) 59 | 60 | def test_set_item(self): 61 | root = SpecialNode() 62 | 63 | with self.assertRaises(TypeError): 64 | root[:] = SpecialNode() 65 | to_insert = [SpecialNode(), SpecialNode()] 66 | 67 | root[:] = to_insert 68 | for c, e in zip(root, to_insert): 69 | self.assertIs(c, e) 70 | self.assertIs(c.parent, root) 71 | 72 | to_overwrite = SpecialNode() 73 | root[1] = to_overwrite 74 | self.assertIs(root[0], to_insert[0]) 75 | self.assertIs(root[1], to_overwrite) 76 | 77 | def test_assert_integrity(self): 78 | root = Node(children=(Node(), Node())) 79 | 80 | root.assert_tree_integrity() 81 | 82 | root_children = getattr(root, '_Node__children') 83 | 84 | root_children[1] = Node() 85 | root_children[1]._Node__parent_index = -1 86 | 87 | root.assert_tree_integrity() 88 | 89 | Node.debug = True 90 | 91 | with self.assertRaises(AssertionError): 92 | root.assert_tree_integrity() 93 | 94 | root_children[1]._Node__parent = weakref.ref(root) 95 | with self.assertRaises(AssertionError): 96 | root.assert_tree_integrity() 97 | 98 | root_children[1]._Node__parent_index = 0 99 | with self.assertRaises(AssertionError): 100 | root.assert_tree_integrity() 101 | 102 | root_children[1]._Node__parent_index = 1 103 | root.assert_tree_integrity() 104 | 105 | def test_depth_iteration(self): 106 | root = Node(children=[Node(children=[Node(), Node()]), Node()]) 107 | 108 | depth_nodes = tuple(root.get_depth_first_iterator()) 109 | 110 | self.assertEqual(depth_nodes, (root[0][0], root[0][1], root[0], root[1], root)) 111 | 112 | def test_breadth_iteration(self): 113 | root = Node(children=[Node(children=[Node(), Node()]), Node()]) 114 | 115 | breadth_nodes = tuple(root.get_breadth_first_iterator()) 116 | 117 | self.assertEqual(breadth_nodes, (root, root[0], root[1], root[0][0], root[0][1])) 118 | 119 | -------------------------------------------------------------------------------- /tests/utils/types_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | from qupulse.utils.types import (HashableNumpyArray, SequenceProxy,) 6 | 7 | 8 | class HashableNumpyArrayTest(unittest.TestCase): 9 | def test_hash(self): 10 | 11 | with self.assertWarns(DeprecationWarning): 12 | a = np.array([1, 2, 3]).view(HashableNumpyArray) 13 | 14 | b = np.array([3, 4, 5]).view(HashableNumpyArray) 15 | 16 | c = np.array([1, 2, 3]).view(HashableNumpyArray) 17 | 18 | self.assertNotEqual(hash(a), hash(b)) 19 | self.assertEqual(hash(a), hash(c)) 20 | 21 | 22 | class SequenceProxyTest(unittest.TestCase): 23 | def test_sequence_proxy(self): 24 | l = [1, 2, 3, 4, 1] 25 | p = SequenceProxy(l) 26 | self.assertEqual(l, list(iter(p))) 27 | self.assertEqual(5, len(p)) 28 | self.assertEqual(3, p[2]) 29 | self.assertEqual(list(reversed(l)), list(reversed(p))) 30 | self.assertEqual(2, p.index(3)) 31 | self.assertEqual(2, p.count(1)) 32 | self.assertIn(3, p) 33 | self.assertNotIn(5, p) 34 | 35 | with self.assertRaises(TypeError): 36 | p[1] = 7 37 | -------------------------------------------------------------------------------- /tests/utils/utils_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | from collections import OrderedDict 4 | 5 | from qupulse.utils import checked_int_cast, replace_multiple, _fallback_pairwise, to_next_multiple 6 | 7 | 8 | class PairWiseTest(unittest.TestCase): 9 | def test_fallback(self): 10 | self.assertEqual([(0, 1), (1, 2), (2, 3), (3, 4)], list(_fallback_pairwise(range(5)))) 11 | self.assertEqual([], list(_fallback_pairwise(range(1)))) 12 | self.assertEqual([], list(_fallback_pairwise(range(0)))) 13 | 14 | 15 | class CheckedIntCastTest(unittest.TestCase): 16 | def test_int_forwarding(self): 17 | my_int = 6 18 | self.assertIs(my_int, checked_int_cast(my_int)) 19 | 20 | def test_no_int_detection(self): 21 | with self.assertRaises(ValueError): 22 | checked_int_cast(0.5) 23 | 24 | with self.assertRaises(ValueError): 25 | checked_int_cast(-0.5) 26 | 27 | with self.assertRaises(ValueError): 28 | checked_int_cast(123124.2) 29 | 30 | with self.assertRaises(ValueError): 31 | checked_int_cast(123124 + 1e-6, epsilon=1e-10) 32 | 33 | def test_float_cast(self): 34 | self.assertEqual(6, checked_int_cast(6+1e-11)) 35 | 36 | self.assertEqual(-6, checked_int_cast(-6 + 1e-11)) 37 | 38 | def test_variable_epsilon(self): 39 | self.assertEqual(6, checked_int_cast(6 + 1e-11)) 40 | 41 | with self.assertRaises(ValueError): 42 | checked_int_cast(6 + 1e-11, epsilon=1e-15) 43 | 44 | 45 | class IsCloseTest(unittest.TestCase): 46 | def test_isclose_fallback(self): 47 | import math 48 | import importlib 49 | import builtins 50 | import qupulse.utils as qutils 51 | 52 | def dummy_is_close(): 53 | pass 54 | 55 | if hasattr(math, 'isclose'): 56 | dummy_is_close = math.isclose 57 | 58 | original_import = builtins.__import__ 59 | 60 | def mock_import_missing_isclose(name, globals=None, locals=None, fromlist=(), level=0): 61 | if name == 'math' and 'isclose' in fromlist: 62 | raise ImportError(name) 63 | else: 64 | return original_import(name, globals=globals, locals=locals, fromlist=fromlist, level=level) 65 | 66 | def mock_import_exsiting_isclose(name, globals=None, locals=None, fromlist=(), level=0): 67 | if name == 'math' and 'isclose' in fromlist: 68 | if not hasattr(math, 'isclose'): 69 | math.isclose = dummy_is_close 70 | return math 71 | else: 72 | return original_import(name, globals=globals, locals=locals, fromlist=fromlist, level=level) 73 | 74 | with mock.patch('builtins.__import__', mock_import_missing_isclose): 75 | reloaded_qutils = importlib.reload(qutils) 76 | self.assertIs(reloaded_qutils.isclose, reloaded_qutils._fallback_is_close) 77 | 78 | with mock.patch('builtins.__import__', mock_import_exsiting_isclose): 79 | reloaded_qutils = importlib.reload(qutils) 80 | self.assertIs(reloaded_qutils.isclose, math.isclose) 81 | 82 | if math.isclose is dummy_is_close: 83 | # cleanup 84 | delattr(math, 'isclose') 85 | 86 | 87 | class ReplacementTests(unittest.TestCase): 88 | def test_replace_multiple(self): 89 | replacements = {'asd': 'dfg', 'dfg': '77', r'\*': '99'} 90 | 91 | text = r'it is asd and dfg that \*' 92 | expected = 'it is dfg and 77 that 99' 93 | result = replace_multiple(text, replacements) 94 | self.assertEqual(result, expected) 95 | 96 | def test_replace_multiple_overlap(self): 97 | replacement_list = [('asd', '1'), ('asdf', '2')] 98 | replacements = OrderedDict(replacement_list) 99 | result = replace_multiple('asdf', replacements) 100 | self.assertEqual(result, '1f') 101 | 102 | replacements = OrderedDict(reversed(replacement_list)) 103 | result = replace_multiple('asdf', replacements) 104 | self.assertEqual(result, '2') 105 | 106 | 107 | class ToNextMultipleTests(unittest.TestCase): 108 | def test_to_next_multiple(self): 109 | from qupulse.utils.types import TimeType 110 | from qupulse.expressions import ExpressionScalar 111 | 112 | duration = TimeType.from_float(47.1415926535) 113 | evaluated = to_next_multiple(sample_rate=TimeType.from_float(2.4),quantum=16)(duration) 114 | expected = ExpressionScalar('160/3') 115 | self.assertEqual(evaluated, expected) 116 | 117 | duration = TimeType.from_float(3.1415926535) 118 | evaluated = to_next_multiple(sample_rate=TimeType.from_float(2.4),quantum=16,min_quanta=13)(duration) 119 | expected = ExpressionScalar('260/3') 120 | self.assertEqual(evaluated, expected) 121 | 122 | duration = 6185240.0000001 123 | evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=13)(duration) 124 | expected = 6185248 125 | self.assertEqual(evaluated, expected) 126 | 127 | duration = 0. 128 | evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=13)(duration) 129 | expected = 0. 130 | self.assertEqual(evaluated, expected) 131 | 132 | duration = ExpressionScalar('abc') 133 | evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=13)(duration).evaluate_in_scope(dict(abc=0.)) 134 | expected = 0. 135 | self.assertEqual(evaluated, expected) 136 | 137 | duration = ExpressionScalar('q') 138 | evaluated = to_next_multiple(sample_rate=ExpressionScalar('w'),quantum=16,min_quanta=1)(duration).evaluate_in_scope( 139 | dict(q=3.14159,w=1.0)) 140 | expected = 16. 141 | self.assertEqual(evaluated, expected) 142 | 143 | --------------------------------------------------------------------------------