From be13938f1f7924cd1d5fcede4fbb8d62d140cece Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 24 Jan 2025 17:23:49 +0100 Subject: [PATCH 1/7] Streamline program concept documentation --- doc/source/concepts/instantiating.rst | 34 ++++++++++++++------------ doc/source/concepts/program.rst | 18 ++++++++++++-- doc/source/concepts/pulsetemplates.rst | 4 +-- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/doc/source/concepts/instantiating.rst b/doc/source/concepts/instantiating.rst index 1ca3edc7c..b20597448 100644 --- a/doc/source/concepts/instantiating.rst +++ b/doc/source/concepts/instantiating.rst @@ -7,19 +7,25 @@ As already briefly mentioned in :ref:`pulsetemplates`, instantiation of pulses i interpretable representation of a concrete pulse ready for execution from the quite high-level :class:`.PulseTemplate` object tree structure that defines parameterizable pulses in qupulse. -This is a two-step process that involves +The entry point is the :meth:`.PulseTemplate.create_program` method of the :class:`.PulseTemplate` hierarchy. +It accepts the pulse parameters, and allows to rename and/or omit channels or measurements. +It checks that the provided parameters and mappings are consistent and meet the optionally defined parameter constraints of the pulse template. +The translation target is defined by the :class:`.ProgramBuilder` argument. -#. Inserting concrete parameter values and obtaining a hardware-independent pulse program tree -#. Flattening that tree, sampling and merging of leaf waveforms according to needs of hardware +Each pulse template knows what program builder methods to call to translate itself. +For example, the :class:`.ConstantPulseTemplate` calls :meth:`.ProgramBuilder.hold` to hold a constant voltage for a limited amount of time while the :class:`.SequncePulseTemplate` forwards the program builder to the sub-templates in order. +The resulting program is completely backend dependent. -This separation allows the first step to be performed in a hardware-agnostic way while the second step does not have -to deal with general functionality and can focus only on hardware-specific tasks. Step 1 is implemented in the -:meth:`.PulseTemplate.create_program` method of the :class:`.PulseTemplate` hierarchy. It checks parameter consistency -with parameter constraints and returns an object of type -:class:`.Loop` which represents a pulse as nested loops of atomic waveforms. This is another object tree structure -but all parameters (including repetition counts) have been substituted by the corresponding numeric values passed into -``create_program``. The :class:`.Loop` object acts as your reference to the instantiated pulse. -See :ref:`/examples/02CreatePrograms.ipynb` for an example on usage of :meth:`.PulseTemplate.create_program`. +**Historically**, there was only a single program type :class:`.Loop` which is still the default output type. +As the time of this writing there is the additional :class:`.LinSpaceProgram` which allows for the efficient representation of linearly spaced voltage changes in arbitrary control structures. There is no established way to handle the latter yet. +The following describes handling of :class:`.Loop` object only via the :class:`.HardwareSetup`. + +The :class:`.Loop` class was designed as a hardware-independent pulse program tree for waveform table based sequencers. +Therefore, the translation into a hardware specific format is a two-step process which consists of the loop object creation as a first step +and the transformation of that tree according to the needs of the hardware as a second step. +However, the AWGs became more flexibly programmable over the years as discussed in :ref:`awgs`. + +The first step of this pulse instantiation is showcased in :ref:`/examples/02CreatePrograms.ipynb` where :meth:`.PulseTemplate.create_program` is used to create a :class:`.Loop` program. The second step of the instantiation is performed by the hardware backend and transparent to the user. Upon registering the pulse with the hardware backend via :meth:`qupulse.hardware.HardwareSetup.register_program`, the backend will determine which @@ -37,8 +43,6 @@ by the driver with its neighbors in the execution sequence until the minimum wav optimizations and merges (or splits) of waveforms for performance are also possible. In contrast, the Zurich Instruments HDAWG allows arbitrary nesting levels and is only limited by the instruction cache. +However, this device supports increment commands which allow the efficient representation of linear voltage sweeps which is **not** possible with the :class:`.Loop` class. -However, as already mentioned, the user does not have to be concerned about this in regular use of qupulse, since this -is dealt with transparently in the hardware backend. - -The section :ref:`program` touches the ideas behind the current program implementation i.e. :class:`.Loop`. +The section :ref:`program` touches the ideas behind the current program implementations i.e. :class:`.Loop` and :class:`.LinSpaceProgram`. diff --git a/doc/source/concepts/program.rst b/doc/source/concepts/program.rst index f607f2e99..cf7a7d47d 100644 --- a/doc/source/concepts/program.rst +++ b/doc/source/concepts/program.rst @@ -9,11 +9,25 @@ The dimensions are named by the channel names. Programs are created by the :meth:`~.PulseTemplate.create_program` method of `PulseTemplate` which returns a hardware independent and un-parameterized representation. The method takes a ``program_builder`` keyword argument that is propagated through the pulse template tree and thereby implements the visitor pattern. -If the argument is not passed :py: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. +If the argument is not passed :func:`~qupulse.program.default_program_builder()` is used instead which is :class:`.LoopBuilder` by default, i.e. the program created by default is of type :class:`.Loop`. The available program builders, programs and their constituents like :class:`.Waveform` and :class:`.VolatileRepetitionCount` are defined in th :mod:`qupulse.program` subpackage and it's submodules. There is a private ``qupulse._program`` subpackage that was used for more rapid iteration development and is slowly phased out. It still contains the hardware specific program representation for the tabor electronics AWG driver. Zurich instrument specific code has been factored into the separate package ``qupulse-hdawg``. Please refer to the reference and the docstrings for exact interfaces and implementation details. The :class:`.Loop` default program is the root node of a tree of loop objects of arbitrary depth. Each node consists of a repetition count and either a waveform or a sequence of nodes which are repeated that many times. Iterations like the :class:`.ForLoopPT` cannot be represented natively but are unrolled into a sequence of items. The repetition count is currently the only property of a program that can be defined as volatile. This means that the AWG driver tries to upload the program in a way, where the repetition count can quickly be changed. This is implemented via the ``VolatileRepetitionCount`` class. -A much more capable program format is :class:`.LinSpaceNode` which efficiently encodes linearly spaced sweeps in voltage space by utilizing increment commands. It is build via :class:`.LinSpaceBuilder`. Increment commands are available in the HDAWG command table. +A much more capable program format is :class:`.LinSpaceNode` which efficiently encodes linearly spaced sweeps in voltage space by utilizing increment commands. It is build via :class:`.LinSpaceBuilder`. +The main complexity of this program class is the efficient handling of interleaved constant points. +The increment and set commands do not only carry a channel and a value but also a dependency key which encodes the dependence of loop indices. +This allows the efficient encoding of + +.. code:: python + for idx in range(10): + set_voltage(CONSTANT) # No dependencies + set_voltage(OFFSET + idx * FACTOR) # depends on idx with + + for _ in range(10): # loop + set_voltage(CONSTANT, key=None) + increment_by(FACTOR, key=(FACTOR,)) + +The motivation is that increment commands with this capability are available in the HDAWG command table. diff --git a/doc/source/concepts/pulsetemplates.rst b/doc/source/concepts/pulsetemplates.rst index 85b7853ad..fcb2b0e55 100644 --- a/doc/source/concepts/pulsetemplates.rst +++ b/doc/source/concepts/pulsetemplates.rst @@ -20,8 +20,6 @@ In some cases, it is desired to write a pulse which partly consists of placehold You can do some simple arithmetic with pulses which is implemented via :class:`.ArithmeticPulseTemplate` and :class:`.ArithmeticAtomicPulseTemplate`. The relevant arithmetic operators are overloaded so you do not need to use these classes directly. -In the future might be pulse templates that allow conditional execution like a `BranchPulseTemplate` or a `WhileLoopPulseTemplate`. - All of these pulse template variants can be similarly accessed through the common interface declared by the :class:`.PulseTemplate` base class. [#pattern]_ As the class names are quite long the recommended way for abbreviation is to use the aliases defined in :py:mod:`~qupulse.pulses`. For example :class:`.FunctionPulseTemplate` is aliased as :class:`.FunctionPT` @@ -52,7 +50,7 @@ To obtain a pulse ready for execution on the hardware from a pulse template, the In order to translate the object structures that encode the pulse template in the software into a (sequential) representation of the concrete pulse with the given parameter values that is understandable by the hardware, we proceed in several steps. -First, the :meth:`.PulseTemplate.create_program` checks parameter consistency with parameter constraints and translates the pulse template into an instantiated program object, which is then further interpreted and sequenced by the hardware backend code (in :py:mod:`~qupulse.hardware`). +First, the :meth:`.PulseTemplate.create_program` checks parameter consistency with parameter constraints and translates the pulse template into an instantiated program object. The nature of this program depends on the targeted hardware and is determined by the ``program_builder`` keyword argument. This program is further interpreted and sequenced by the hardware backend code (in :py:mod:`~qupulse.hardware`). See :ref:`instantiating` for a more in-depth explanation of instantiating pulses. From e7a93871d3ebdc8799e1b5efaf3cc1b2542edea6 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 24 Jan 2025 18:00:30 +0100 Subject: [PATCH 2/7] Fix errors in concepts --- doc/source/concepts/instantiating.rst | 2 +- doc/source/concepts/program.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/concepts/instantiating.rst b/doc/source/concepts/instantiating.rst index b20597448..7af5aac7b 100644 --- a/doc/source/concepts/instantiating.rst +++ b/doc/source/concepts/instantiating.rst @@ -13,7 +13,7 @@ It checks that the provided parameters and mappings are consistent and meet the The translation target is defined by the :class:`.ProgramBuilder` argument. Each pulse template knows what program builder methods to call to translate itself. -For example, the :class:`.ConstantPulseTemplate` calls :meth:`.ProgramBuilder.hold` to hold a constant voltage for a limited amount of time while the :class:`.SequncePulseTemplate` forwards the program builder to the sub-templates in order. +For example, the :class:`.ConstantPulseTemplate` calls :meth:`.ProgramBuilder.hold_voltage` to hold a constant voltage for a defined amount of time while the :class:`.SequncePulseTemplate` forwards the program builder to the sub-templates in order. The resulting program is completely backend dependent. **Historically**, there was only a single program type :class:`.Loop` which is still the default output type. diff --git a/doc/source/concepts/program.rst b/doc/source/concepts/program.rst index cf7a7d47d..8591309bd 100644 --- a/doc/source/concepts/program.rst +++ b/doc/source/concepts/program.rst @@ -22,6 +22,7 @@ The increment and set commands do not only carry a channel and a value but also This allows the efficient encoding of .. code:: python + for idx in range(10): set_voltage(CONSTANT) # No dependencies set_voltage(OFFSET + idx * FACTOR) # depends on idx with From 2e4f4b03fda49b94617193b0550f09086bdc5118 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 24 Jan 2025 18:00:49 +0100 Subject: [PATCH 3/7] Improve ProgramBuilder docstrings --- qupulse/program/__init__.py | 73 ++++++++++++++++++++++++++++++------- 1 file changed, 59 insertions(+), 14 deletions(-) diff --git a/qupulse/program/__init__.py b/qupulse/program/__init__.py index a52f9edcf..0671a7afe 100644 --- a/qupulse/program/__init__.py +++ b/qupulse/program/__init__.py @@ -118,42 +118,87 @@ def duration(self) -> TimeType: class ProgramBuilder(Protocol): - """This protocol is used by :py:meth:`.PulseTemplate.create_program` to build a program via the visitor pattern. + """This protocol is used by :py:meth:`.PulseTemplate.create_program` to build a program via a variation of the + visitor pattern. - There is a default implementation which is the loop class. + The pulse templates call the methods that correspond to their functionality on the program builder. For example, + :py:class:`.ConstantPulseTemplate` translates itself into a simple :py:meth:`.ProgramBuilder.hold_voltage` call while + :class:`SequencePulseTemplate` uses :py:meth:`.ProgramBuilder.with_sequence` to signify a logical unit with + attached measurements and passes the resulting object to the sequenced sub-templates. - Other hardware backends can use this protocol to implement easy translation of pulse templates into a hardware - compatible format.""" + Due to backward compatibility the handling of measurements is a bit weird since they have to be omitted in certain + cases. However, this is not relevant for HDAWG specific implementations because these are expected to ignore + :py:meth:`.ProgramBuilder.measure` calls. + + This interface makes heavy use of context managers and generators/iterators which allows for flexible iteration + and repetition implementation. + """ def inner_scope(self, scope: Scope) -> Scope: - """This function is necessary to inject program builder specific parameter implementations into the build - process.""" + """This function is part of the iteration protocol and necessary to inject program builder specific parameter + implementations into the build process. :py:meth:`.ProgramBuilder.with_iteration` and + `.ProgramBuilder.with_iteration` callers *must* call this function inside the iteration. + + Args: + scope: The parameter scope outside the iteration. + + Returns: + The parameter scope inside the iteration. + """ def hold_voltage(self, duration: HardwareTime, voltages: Mapping[str, HardwareVoltage]): - """Supports dynamic i.e. for loop generated offsets and duration""" + """Hold the specified voltage for a given time. Advances the current time by ``duration``. The values are + hardware dependent type which are inserted into the parameter scope via :py:meth:`.ProgramBuilder.with_iteration`. + + Args: + duration: Duration of voltage hold + voltages: Voltages for each channel + """ - # further specialized commandos like play_harmoic might be added here + # further specialized commandos like play_harmonic might be added here def play_arbitrary_waveform(self, waveform: Waveform): - """""" + """Insert the playback of an arbitrary waveform. If possible pulse templates should use more specific commands + like :py:meth:`.ProgramBuilder.hold_voltage` (the only more specific command at the time of this writing). + + Args: + waveform: The waveform to play + """ def measure(self, measurements: Optional[Sequence[MeasurementWindow]]): - """Unconditionally add given measurements relative to the current position.""" + """Unconditionally add given measurements relative to the current position. + + Args: + measurements: Measurements to add. + """ def with_repetition(self, repetition_count: RepetitionCount, measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']: - """Measurements that are added to the new builder are dropped if the builder is empty upon exit""" + """Start a new repetition context with given repetition count. The caller has to iterate over the return value + and call `:py:meth:`.ProgramBuilder.inner_scope` inside the iteration context. + + Args: + repetition_count: Repetition count + measurements: These measurements are added relative to the position at the start of the iteration iff the + iteration is not empty. + + Returns: + An iterable of :py:class:`ProgramBuilder` instances. + """ def with_sequence(self, measurements: Optional[Sequence[MeasurementWindow]] = None) -> ContextManager['ProgramBuilder']: - """ + """Start a new sequence context. The caller has to enter the returned context manager and add the sequenced + elements there. Measurements that are added in to the returned program builder are discarded if the sequence is empty on exit. Args: - measurements: Measurements to attach to the potential child. Is not repeated with repetition_count. - repetition_count: + measurements: These measurements are added relative to the position at the start of the sequence iff the + sequence is not empty. + Returns: + A context manager that returns a :py:class:`ProgramBuilder` on entering. """ def new_subprogram(self, global_transformation: 'Transformation' = None) -> ContextManager['ProgramBuilder']: From 96f1c14ac917e59b16a2c5adc20f7de008d8a735 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 24 Jan 2025 18:01:18 +0100 Subject: [PATCH 4/7] Include another explicit pandoc mention in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eef617c4c..34ce28352 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ The backend for TaborAWGs requires packages that can be found [here](https://git The data acquisition backend for AlazarTech cards needs a package that unfortunately is not open source (yet). If you need it or have questions contact . ## Documentation -You can find documentation on how to use this library on [readthedocs](https://qupulse.readthedocs.io/en/latest/) and [IPython notebooks with examples in this repo](doc/source/examples). You can build it locally with `hatch run docs:html`. +You can find documentation on how to use this library on [readthedocs](https://qupulse.readthedocs.io/en/latest/) and [IPython notebooks with examples in this repo](doc/source/examples). You can build it locally with `hatch run docs:html` if you have pandoc installed. ### Folder Structure The repository primarily consists of the folders `qupulse` (source code), `tests` and `doc`. From 738c882a8bf80a5205294249a476abf16ac511e4 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Sat, 25 Jan 2025 16:31:10 +0100 Subject: [PATCH 5/7] Greatly improve program documentation --- qupulse/_program/__init__.py | 5 +- qupulse/program/__init__.py | 127 +++++++++++++++++++++++++++++------ 2 files changed, 111 insertions(+), 21 deletions(-) diff --git a/qupulse/_program/__init__.py b/qupulse/_program/__init__.py index 5f2427b92..896924d6e 100644 --- a/qupulse/_program/__init__.py +++ b/qupulse/_program/__init__.py @@ -2,4 +2,7 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -"""This is a private package meaning there are no stability guarantees.""" +"""This is a private package meaning there are no stability guarantees. + +Large parts of this package where stabilized and live now in :py:mod:`qupulse.program`. +""" diff --git a/qupulse/program/__init__.py b/qupulse/program/__init__.py index 0671a7afe..e2f1b4f8d 100644 --- a/qupulse/program/__init__.py +++ b/qupulse/program/__init__.py @@ -2,19 +2,38 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -import contextlib -from abc import ABC, abstractmethod +"""This package contains the means to construct a program from a pulse template. + +A program is an un-parameterized multichannel time to voltage relation. They are constructed by sequencing playback +commands which typically mean that an arbitrary waveform is played. + +The arbitrary waveforms are defined in the :py:mod:`.waveforms` module. + +:py:mod:`.transformation` contains useful transformations for waveforms which for example allow the +construction of virtual channels, i.e. linear combinations of channels from a set of other channes. + +:py:mod:`.loop` contains the legacy program representation with is an aribtrariliy nested sequence/repetition structure +of waveform playbacks. + +:py:mod:`.linspace` contains a more modern program representation to efficiently execute linearly spaced voltage sweeps +even if interleaved with constant segments. + +:py:mod:`.volatile` contains the machinery to declare quickly changable program parameters. This functionality is stale +and was not used by the library authors for a long term. It is very useful for dynamic nuclear polarization which is not +used/required/possible with (purified) silicon samples. +""" + +from abc import abstractmethod from dataclasses import dataclass from typing import Optional, Union, Sequence, ContextManager, Mapping, Tuple, Generic, TypeVar, Iterable, Dict -from numbers import Real, Number - -import numpy as np +from numbers import Real -from qupulse._program.waveforms import Waveform -from qupulse.utils.types import MeasurementWindow, TimeType, FrozenMapping -from qupulse._program.volatile import VolatileRepetitionCount +from qupulse.program.waveforms import Waveform +from qupulse.program.transformation import Transformation +from qupulse.program.volatile import VolatileRepetitionCount +from qupulse.utils.types import MeasurementWindow, TimeType from qupulse.parameter_scope import Scope -from qupulse.expressions import sympy as sym_expr, Expression +from qupulse.expressions import sympy as sym_expr from qupulse.utils.sympy import _lambdify_modules from typing import Protocol, runtime_checkable @@ -32,6 +51,14 @@ class SimpleExpression(Generic[NumVal]): The main use case is the expression of for loop dependent variables where the Rs are loop indices. There the expressions can be calculated via simple increments. + + This class tries to pass a number and a :py:class:`sympy.expr.Expr` on + best effort basis. + + Attributes: + base: The part of this expression which is not runtime parameter dependent + offsets: Factors would have been a better name in hindsight. A mapping of inner parameter names to the factor + with which they contribute to the final value. """ base: NumVal @@ -41,6 +68,12 @@ def __post_init__(self): assert isinstance(self.offsets, Mapping) def value(self, scope: Mapping[str, NumVal]) -> NumVal: + """Numeric value of the expression with the given scope. + Args: + scope: Scope in which the expression is evaluated. + Returns: + The numeric value. + """ value = self.base for name, factor in self.offsets: value += scope[name] * factor @@ -51,11 +84,12 @@ def __add__(self, other): return SimpleExpression(self.base + other, self.offsets) if type(other) == type(self): - offsets = self.offsets.copy() + offsets = dict(self.offsets) for name, value in other.offsets.items(): offsets[name] = value + offsets.get(name, 0) return SimpleExpression(self.base + other.base, offsets) + # this defers evaluation when other is still a symbolic expression return NotImplemented def __radd__(self, other): @@ -74,6 +108,7 @@ def __mul__(self, other: NumVal): if isinstance(other, (float, int, TimeType)): return SimpleExpression(self.base * other, {name: other * value for name, value in self.offsets.items()}) + # this defers evaluation when other is still a symbolic expression return NotImplemented def __rmul__(self, other): @@ -85,20 +120,33 @@ def __truediv__(self, other): @property def free_symbols(self): + """This is required for the :py:class:`sympy.expr.Expr` interface compliance. Since the keys of + :py:attr:`.offsets` are internal parameters we do not have free symbols. + + Returns: + An empty tuple + """ return () def _sympy_(self): + """This method is used by :py:`sympy.sympify`. This class tries to "just work" in the sympy evaluation pipelines. + + Returns: + self + """ return self def replace(self, r, s): - return self + """We mock :class:`sympy.Expr.replace` here. This class does not support inner parameters so there is nothing + to replace. Importantly, the keys of the offsets are no runtime variables! - def evaluate_in_scope_(self, *args, **kwargs): - # TODO: remove. It is currently required to avoid nesting this class in an expression for the MappedScope - # We can maybe replace is with a HardwareScope or something along those lines + Returns: + self + """ return self +# this keeps the simple expression in lambdified results _lambdify_modules.append({'SimpleExpression': SimpleExpression}) @@ -113,10 +161,12 @@ class Program(Protocol): because most of a program class' capability are specific to the implementation.""" @property + @abstractmethod def duration(self) -> TimeType: - raise NotImplementedError() + """The duration of the program in nanoseconds.""" +@runtime_checkable class ProgramBuilder(Protocol): """This protocol is used by :py:meth:`.PulseTemplate.create_program` to build a program via a variation of the visitor pattern. @@ -134,6 +184,7 @@ class ProgramBuilder(Protocol): and repetition implementation. """ + @abstractmethod def inner_scope(self, scope: Scope) -> Scope: """This function is part of the iteration protocol and necessary to inject program builder specific parameter implementations into the build process. :py:meth:`.ProgramBuilder.with_iteration` and @@ -146,6 +197,7 @@ def inner_scope(self, scope: Scope) -> Scope: The parameter scope inside the iteration. """ + @abstractmethod def hold_voltage(self, duration: HardwareTime, voltages: Mapping[str, HardwareVoltage]): """Hold the specified voltage for a given time. Advances the current time by ``duration``. The values are hardware dependent type which are inserted into the parameter scope via :py:meth:`.ProgramBuilder.with_iteration`. @@ -157,6 +209,7 @@ def hold_voltage(self, duration: HardwareTime, voltages: Mapping[str, HardwareVo # further specialized commandos like play_harmonic might be added here + @abstractmethod def play_arbitrary_waveform(self, waveform: Waveform): """Insert the playback of an arbitrary waveform. If possible pulse templates should use more specific commands like :py:meth:`.ProgramBuilder.hold_voltage` (the only more specific command at the time of this writing). @@ -165,6 +218,7 @@ def play_arbitrary_waveform(self, waveform: Waveform): waveform: The waveform to play """ + @abstractmethod def measure(self, measurements: Optional[Sequence[MeasurementWindow]]): """Unconditionally add given measurements relative to the current position. @@ -172,6 +226,7 @@ def measure(self, measurements: Optional[Sequence[MeasurementWindow]]): measurements: Measurements to add. """ + @abstractmethod def with_repetition(self, repetition_count: RepetitionCount, measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']: """Start a new repetition context with given repetition count. The caller has to iterate over the return value @@ -186,6 +241,7 @@ def with_repetition(self, repetition_count: RepetitionCount, An iterable of :py:class:`ProgramBuilder` instances. """ + @abstractmethod def with_sequence(self, measurements: Optional[Sequence[MeasurementWindow]] = None) -> ContextManager['ProgramBuilder']: """Start a new sequence context. The caller has to enter the returned context manager and add the sequenced @@ -201,23 +257,54 @@ def with_sequence(self, A context manager that returns a :py:class:`ProgramBuilder` on entering. """ + @abstractmethod def new_subprogram(self, global_transformation: 'Transformation' = None) -> ContextManager['ProgramBuilder']: """Create a context managed program builder whose contents are translated into a single waveform upon exit if - it is not empty.""" + it is not empty. + + Args: + global_transformation: This transformation is applied to the waveform + + Returns: + A context manager that returns a :py:class:`ProgramBuilder` on entering. + """ + @abstractmethod def with_iteration(self, index_name: str, rng: range, measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']: - pass + """Create an iterable that represent the body of the iteration. This can be an iterable with an element for each + step in the iteration or a single object that represents the complete iteration. + + Args: + index_name: The name of index + rng: The range if the index + measurements: Measurements to add iff the iteration body is not empty. + """ + @abstractmethod def time_reversed(self) -> ContextManager['ProgramBuilder']: - pass + """This returns a new context manager that will reverse everything added to it in time upon exit. + + Returns: + A context manager that returns a :py:class:`ProgramBuilder` on entering. + """ + @abstractmethod def to_program(self) -> Optional[Program]: - """Further addition of new elements might fail after finalizing the program.""" + """Generate the final program. This is allowed to invalidate the program builder. + + Returns: + A program implementation. None if nothing was added to this program builder. + """ def default_program_builder() -> ProgramBuilder: - """This function returns an instance of the default program builder class `LoopBuilder`""" + """This function returns an instance of the default program builder class :class:`.LoopBuilder` in the default + configuration. + + Returns: + A program builder instance. + """ from qupulse.program.loop import LoopBuilder return LoopBuilder() From d2507dffc9e42720a5648337e10edc145492d166 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Sat, 25 Jan 2025 16:39:24 +0100 Subject: [PATCH 6/7] Improve type annotations and documentation in linspace --- qupulse/program/linspace.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/qupulse/program/linspace.py b/qupulse/program/linspace.py index ad681a96f..d5244d532 100644 --- a/qupulse/program/linspace.py +++ b/qupulse/program/linspace.py @@ -2,12 +2,12 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -import abc import contextlib import dataclasses import numpy as np from dataclasses import dataclass -from typing import Mapping, Optional, Sequence, ContextManager, Iterable, Tuple, Union, Dict, List, Iterator +from abc import ABC, abstractmethod +from typing import Mapping, Optional, Sequence, ContextManager, Iterable, Tuple, Union, Dict, List, Set from qupulse import ChannelID, MeasurementWindow from qupulse.parameter_scope import Scope, MappedScope, FrozenDict @@ -38,10 +38,17 @@ def from_voltages(cls, voltages: Sequence[float], resolution: float): @dataclass -class LinSpaceNode: +class LinSpaceNode(ABC): """AST node for a program that supports linear spacing of set points as well as nested sequencing and repetitions""" - def dependencies(self) -> Mapping[int, set]: + @abstractmethod + def dependencies(self) -> Mapping[int, Set[Tuple[float, ...]]]: + """Returns a mapping from channel indices to the iteration indices dependencies that those channels have inside + this node. + + Returns: + Mapping from channel indices to the iteration indices dependencies + """ raise NotImplementedError def reversed(self, offset: int, lengths: list): @@ -107,6 +114,9 @@ class LinSpaceArbitraryWaveform(LinSpaceNode): waveform: Waveform channels: Tuple[ChannelID, ...] + def dependencies(self) -> Mapping[int, Set[Tuple[float, ...]]]: + return {} + def reversed(self, offset: int, lengths: list): return LinSpaceArbitraryWaveform( waveform=self.waveform.reversed(), From 9e892a3a008638bb0ff9d7784474aeff85270401 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Sat, 25 Jan 2025 16:44:24 +0100 Subject: [PATCH 7/7] Fix doc compilation errors --- doc/source/concepts/awgs.rst | 2 +- doc/source/concepts/instantiating.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/concepts/awgs.rst b/doc/source/concepts/awgs.rst index accd604d3..e51fc45a0 100644 --- a/doc/source/concepts/awgs.rst +++ b/doc/source/concepts/awgs.rst @@ -1,4 +1,4 @@ -.. _hardware: +.. _awgs: How qupulse models AWGs ----------------------- diff --git a/doc/source/concepts/instantiating.rst b/doc/source/concepts/instantiating.rst index 7af5aac7b..387db5db6 100644 --- a/doc/source/concepts/instantiating.rst +++ b/doc/source/concepts/instantiating.rst @@ -18,7 +18,7 @@ The resulting program is completely backend dependent. **Historically**, there was only a single program type :class:`.Loop` which is still the default output type. As the time of this writing there is the additional :class:`.LinSpaceProgram` which allows for the efficient representation of linearly spaced voltage changes in arbitrary control structures. There is no established way to handle the latter yet. -The following describes handling of :class:`.Loop` object only via the :class:`.HardwareSetup`. +The following describes handling of :class:`.Loop` object only via the :class:`qupulse.hardware.HardwareSetup`. The :class:`.Loop` class was designed as a hardware-independent pulse program tree for waveform table based sequencers. Therefore, the translation into a hardware specific format is a two-step process which consists of the loop object creation as a first step