qprogram-qblox 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,162 @@
1
+ # Copyright 2026 Qilimanjaro Quantum Tech
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Qblox vendor extensions for QProgram.
15
+
16
+ This package provides:
17
+
18
+ 1. **Runtime registration**: importing this package registers the ``qblox`` vendor namespace on
19
+ [`QProgram`][qprogram.QProgram], registers every Qblox operation with the ``.qp`` serializer, and
20
+ registers the ``qblox-default-v1`` capability profile.
21
+
22
+ 2. **Typed mixin**: [`QbloxMixin`][qprogram_qblox.QbloxMixin] adds a typed ``.qblox`` property for IDE autocomplete.
23
+
24
+ 3. **Pre-combined QProgram**: [`QProgram`][qprogram_qblox.QProgram] from this package has ``.qblox`` typed out of the
25
+ box.
26
+
27
+ The import is the activation step, and a caller reading a ``.qp`` file need not perform it: the
28
+ ``qprogram.vendors`` entry point this package declares lets `qprogram.loads` import the module
29
+ on demand when it meets a ``require qblox`` header.
30
+
31
+ The operations span both kinds a vendor extension can offer, since QProgram draws no line between
32
+ real-time and host-side execution:
33
+
34
+ - one-to-one sequencer instructions ([`QbloxNamespace.acquire`][qprogram_qblox.QbloxNamespace.acquire],
35
+ [`QbloxNamespace.set_markers`][qprogram_qblox.QbloxNamespace.set_markers],
36
+ [`QbloxNamespace.set_trigger`][qprogram_qblox.QbloxNamespace.set_trigger],
37
+ [`QbloxNamespace.wait_trigger`][qprogram_qblox.QbloxNamespace.wait_trigger]);
38
+ - host-side-only operations
39
+ ([`QbloxNamespace.set_acquisition_threshold`][qprogram_qblox.QbloxNamespace.set_acquisition_threshold],
40
+ [`QbloxNamespace.set_acquisition_rotation`][qprogram_qblox.QbloxNamespace.set_acquisition_rotation]), each a
41
+ slow-control parameter write at execution time rather than a sequencer instruction.
42
+
43
+ Both spell the same way in ``.qp`` (``qblox.<op_name> <args>``); the platform decides at execution
44
+ time how to realize each one. A third kind is possible and is not among the operations here: an
45
+ orchestration that lowers to several instructions. Routing a measurement result back to a drive
46
+ sequencer, for instance, is a property of the platform wiring rather than of the qblox instrument
47
+ API, so it belongs to a platform package instead.
48
+
49
+ The shortest way in is the pre-combined class::
50
+
51
+ from qprogram_qblox import QProgram
52
+
53
+ qp = QProgram(label="example")
54
+ qp.qblox.acquire("readout_q0", "weights")
55
+ qp.qblox.set_markers("drive_q0", "0001")
56
+ qp.qblox.set_acquisition_threshold("readout_q0", value=0.42)
57
+ qp.qblox.set_acquisition_rotation("readout_q0", angle=0.7854)
58
+
59
+ Combining several vendors means listing their mixins in the MRO::
60
+
61
+ from qprogram import QProgram as BaseQProgram
62
+ from qprogram_qblox import QbloxMixin
63
+ from qprogram_qdac import QdacMixin
64
+
65
+
66
+ class QProgram(QbloxMixin, QdacMixin, BaseQProgram):
67
+ pass
68
+
69
+
70
+ qp = QProgram()
71
+ qp.qblox.acquire(...)
72
+ qp.qdac.play(...)
73
+
74
+ A ``.qp`` file carrying qblox operations names the vendor in its header::
75
+
76
+ #!QProgram 1.0
77
+
78
+ require qblox 0.1
79
+
80
+ body:
81
+ qblox.acquire "readout_q0" "weights"
82
+ qblox.set_markers "drive_q0" "0001"
83
+ """
84
+
85
+ from importlib.metadata import PackageNotFoundError, version
86
+
87
+ from qprogram.qprogram import QProgram as _BaseQProgram
88
+ from qprogram.serialization._specs import make_measurement_op_parse, measurement_op_serialize
89
+ from qprogram.serialization.registry import register_vendor_operation, register_vendor_version
90
+
91
+ from qprogram_qblox.mixin import QbloxMixin
92
+ from qprogram_qblox.namespace import QbloxNamespace
93
+ from qprogram_qblox.operations import (
94
+ Acquire,
95
+ SetAcquisitionRotation,
96
+ SetAcquisitionThreshold,
97
+ SetMarkers,
98
+ SetTrigger,
99
+ WaitTrigger,
100
+ )
101
+ from qprogram_qblox.profiles import QBLOX_DEFAULT_V1
102
+ from qprogram_qblox.profiles import _register as _register_qblox_profile
103
+
104
+ # Resolve our own package version once. This is the single source of truth for the qblox vendor
105
+ # protocol version: parsers check that a file's `require qblox <major.minor>` is compatible with
106
+ # this number.
107
+ try:
108
+ __version__ = version("qprogram-qblox")
109
+ except PackageNotFoundError: # pragma: no cover - source tree without installed metadata
110
+ __version__ = "0.0.0"
111
+
112
+ # --- Step 1: Register the vendor namespace on base QProgram ---
113
+ # Registering on the base class is what makes program.qblox.<method>() work on any QProgram
114
+ # instance, mixin or not.
115
+ _BaseQProgram.register_vendor("qblox", QbloxNamespace)
116
+
117
+ # --- Step 2: Register the protocol version of this vendor extension ---
118
+ # The .qp parser checks `require qblox <x.y>` against it.
119
+ register_vendor_version("qblox", __version__)
120
+
121
+ # --- Step 3: Register operations with the .qp serializer ---
122
+ # All but `acquire` use the default signature-driven serialize/parse pair. `acquire` is a
123
+ # measurement operation, so it needs the measurement callbacks that carry the handle name across
124
+ # the wire as a `name="..."` kwarg.
125
+ register_vendor_operation(
126
+ "qblox",
127
+ "acquire",
128
+ Acquire,
129
+ serialize=measurement_op_serialize,
130
+ parse=make_measurement_op_parse(Acquire),
131
+ )
132
+ register_vendor_operation("qblox", "set_markers", SetMarkers)
133
+ register_vendor_operation("qblox", "set_trigger", SetTrigger)
134
+ register_vendor_operation("qblox", "wait_trigger", WaitTrigger)
135
+ register_vendor_operation("qblox", "set_acquisition_threshold", SetAcquisitionThreshold)
136
+ register_vendor_operation("qblox", "set_acquisition_rotation", SetAcquisitionRotation)
137
+
138
+ # --- Step 4: Register the qblox capability profile bundle ---
139
+ # Importing qprogram_qblox.profiles above already registered the vendor capability tokens, which is
140
+ # what lets the profile name them.
141
+ _register_qblox_profile()
142
+
143
+
144
+ class QProgram(QbloxMixin, _BaseQProgram):
145
+ """[`QProgram`][qprogram.QProgram] pre-combined with [`QbloxMixin`][qprogram_qblox.QbloxMixin].
146
+
147
+ Identical to [`qprogram.QProgram`][] but with IDE autocomplete for ``qp.qblox.*``.
148
+ """
149
+
150
+
151
+ __all__ = [
152
+ "QBLOX_DEFAULT_V1",
153
+ "Acquire",
154
+ "QProgram",
155
+ "QbloxMixin",
156
+ "QbloxNamespace",
157
+ "SetAcquisitionRotation",
158
+ "SetAcquisitionThreshold",
159
+ "SetMarkers",
160
+ "SetTrigger",
161
+ "WaitTrigger",
162
+ ]
@@ -0,0 +1,73 @@
1
+ # Copyright 2026 Qilimanjaro Quantum Tech
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Typed mixin that exposes the Qblox namespace as ``.qblox`` on a QProgram subclass.
15
+
16
+ The mixin exists for the benefit of editors and type-checkers. At runtime the base
17
+ [`QProgram`][qprogram.QProgram]'s dynamic ``__getattr__`` already routes ``program.qblox.*`` to the
18
+ registered [`QbloxNamespace`][qprogram_qblox.QbloxNamespace], but static tooling cannot see that
19
+ dispatch, so the mixin spells the namespace out as a typed ``@property``.
20
+
21
+ One vendor, using the pre-combined class this package ships::
22
+
23
+ from qprogram_qblox import QProgram
24
+
25
+ qp = QProgram() # QProgram with .qblox typed
26
+ qp.qblox.acquire(...) # IDE autocomplete works
27
+
28
+ Several vendors, composed by listing their mixins in the MRO::
29
+
30
+ from qprogram import QProgram as BaseQProgram
31
+ from qprogram_qblox import QbloxMixin
32
+ from qprogram_qdac import QdacMixin
33
+
34
+
35
+ class QProgram(QbloxMixin, QdacMixin, BaseQProgram):
36
+ pass
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ from typing import TYPE_CHECKING
42
+
43
+ from qprogram_qblox.namespace import QbloxNamespace
44
+
45
+ if TYPE_CHECKING:
46
+ from qprogram.qprogram import QProgram as _BaseQProgram
47
+
48
+
49
+ class QbloxMixin:
50
+ """Mixin that adds a typed ``.qblox`` property to QProgram.
51
+
52
+ Compose it with [`qprogram.QProgram`][] through multiple inheritance to get editor
53
+ autocomplete for the Qblox operations. The property caches the namespace on the program, so
54
+ repeated ``program.qblox`` accesses return the same object.
55
+ """
56
+
57
+ @property
58
+ def qblox(self: _BaseQProgram) -> QbloxNamespace: # type: ignore[misc]
59
+ """This program's typed Qblox namespace.
60
+
61
+ The first access builds a [`QbloxNamespace`][qprogram_qblox.QbloxNamespace] bound to the
62
+ program and stores it under a private attribute; later accesses return that same instance.
63
+ Both the load and the store go through `object` so they bypass the program's own
64
+ attribute hooks: a cache miss has to surface as a plain `AttributeError` here rather
65
+ than reach [`QProgram`][qprogram.QProgram]'s vendor-registry ``__getattr__``.
66
+ """
67
+ try:
68
+ return object.__getattribute__(self, "_qblox_ns") # ruff: ignore[unnecessary-dunder-call]
69
+ except AttributeError:
70
+ pass
71
+ ns = QbloxNamespace(self)
72
+ object.__setattr__(self, "_qblox_ns", ns) # ruff: ignore[unnecessary-dunder-call]
73
+ return ns
@@ -0,0 +1,187 @@
1
+ # Copyright 2026 Qilimanjaro Quantum Tech
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Typed [`VendorNamespace`][qprogram.VendorNamespace] for Qblox operations.
15
+
16
+ Each method on [`QbloxNamespace`][qprogram_qblox.QbloxNamespace] is a typed wrapper that builds the matching
17
+ [`Operation`][qprogram.operations.Operation] subclass from `qprogram_qblox.operations` and appends
18
+ it to the program's active block. This is where explicit parameter types live, so editors complete
19
+ and type-checkers check ``program.qblox.<operation>(...)``. The dynamic ``__getattr__`` on
20
+ [`QProgram`][qprogram.QProgram] dispatches the same calls at runtime; the typed namespace is what makes
21
+ them discoverable.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from typing import TYPE_CHECKING
27
+
28
+ from qprogram.operations.operation import MeasurementField
29
+ from qprogram.vendor import VendorNamespace
30
+
31
+ from qprogram_qblox.operations import (
32
+ Acquire,
33
+ SetAcquisitionRotation,
34
+ SetAcquisitionThreshold,
35
+ SetMarkers,
36
+ SetTrigger,
37
+ WaitTrigger,
38
+ )
39
+
40
+ if TYPE_CHECKING:
41
+ from collections.abc import Iterable
42
+
43
+ from qprogram.result import MeasurementHandle
44
+ from qprogram.variable import Expression
45
+ from qprogram.waveforms.waveform import IQWaveform
46
+
47
+
48
+ class QbloxNamespace(VendorNamespace):
49
+ """Qblox vendor namespace, reached as ``program.qblox.<operation>()``.
50
+
51
+ Attached to every [`QProgram`][qprogram.QProgram] instance as ``.qblox`` once the
52
+ `qprogram_qblox` package is imported. Each method validates its arguments through the typed
53
+ signature, constructs the matching operation, and appends it to the program's currently active
54
+ block.
55
+ """
56
+
57
+ def acquire(
58
+ self,
59
+ bus: str,
60
+ weights: IQWaveform | str,
61
+ fields: Iterable[MeasurementField] = (MeasurementField.IQ,),
62
+ *,
63
+ name: str | None = None,
64
+ ) -> MeasurementHandle:
65
+ """Append an [`Acquire`][qprogram_qblox.Acquire] operation.
66
+
67
+ The handle's name comes from the same per-bus counter core
68
+ [`measure`][qprogram.QProgram.measure] draws on, so two acquisitions on ``q[0].readout`` produce
69
+ ``q0/readout/m0`` and ``q0/readout/m1`` whether or not a core ``measure`` also runs on that
70
+ qubit. A raw-string bus carries no coordinates to build that prefix from, so it falls back
71
+ to the bare ``m0``, ``m1``, ... counter shared by every raw-string measurement.
72
+
73
+ Args:
74
+ bus (str): Readout bus to acquire on.
75
+ weights (IQWaveform | str): Integration weights, either a concrete
76
+ [`IQWaveform`][qprogram.waveforms.IQWaveform] or a string alias.
77
+ fields (Iterable[MeasurementField]): Which measurement fields to produce, as an iterable
78
+ of [`MeasurementField`][qprogram.MeasurementField] members. Default ``(MeasurementField.IQ,)``;
79
+ `RAW` asks for the raw ADC trace.
80
+ name (str | None): Explicit measurement name. Auto-allocated when omitted.
81
+
82
+ Returns:
83
+ The [`MeasurementHandle`][qprogram.MeasurementHandle] this acquisition writes to, for retrieving the
84
+ result and for referencing the outcome in a conditional.
85
+
86
+ Raises:
87
+ ValidationError: If ``name`` is empty, is not a string, or is already used by another
88
+ measurement in the program, if ``fields`` names a field that is not registered, or
89
+ if ``bus`` is a [`BusRef`][qprogram.BusRef] from another schema than the one attached
90
+ to the program.
91
+ """
92
+ return self._append_measurement(
93
+ Acquire,
94
+ bus=bus,
95
+ weights=weights,
96
+ fields=fields,
97
+ name=name,
98
+ )
99
+
100
+ def set_markers(self, bus: str, mask: str) -> None:
101
+ """Append a [`SetMarkers`][qprogram_qblox.SetMarkers] operation.
102
+
103
+ Args:
104
+ bus (str): Bus whose marker outputs to drive.
105
+ mask (str): Four characters of ``0`` and ``1``, one per marker line, e.g. ``"0001"``.
106
+
107
+ Raises:
108
+ ValidationError: If ``bus`` is a [`BusRef`][qprogram.BusRef] from another schema than the
109
+ one attached to the program.
110
+ """
111
+ self._append(SetMarkers(bus=bus, mask=mask))
112
+
113
+ def set_trigger(
114
+ self,
115
+ bus: str,
116
+ duration: int,
117
+ outputs: list[int] | int | None = None,
118
+ position: str = "start",
119
+ ) -> None:
120
+ """Append a [`SetTrigger`][qprogram_qblox.SetTrigger] operation.
121
+
122
+ Args:
123
+ bus (str): Bus whose trigger outputs to arm.
124
+ duration (int): Trigger-active duration in nanoseconds.
125
+ outputs (list[int] | int | None): Trigger output indices to arm, one index or a list of
126
+ them. ``None`` leaves the selection to the platform.
127
+ position (str): Point in the operation at which the trigger fires, either ``"start"`` or
128
+ ``"end"``. Default ``"start"``.
129
+
130
+ Raises:
131
+ ValidationError: If ``bus`` is a [`BusRef`][qprogram.BusRef] from another schema than the
132
+ one attached to the program.
133
+ """
134
+ self._append(SetTrigger(bus=bus, duration=duration, outputs=outputs, position=position))
135
+
136
+ def wait_trigger(self, bus: str, duration: int, port: int | None = None) -> None:
137
+ """Append a [`WaitTrigger`][qprogram_qblox.WaitTrigger] operation.
138
+
139
+ Args:
140
+ bus (str): Bus whose sequencer waits.
141
+ duration (int): Timeout in nanoseconds, after which the sequencer stops waiting.
142
+ port (int | None): Trigger input port to listen on. ``None`` leaves the choice to the
143
+ platform.
144
+
145
+ Raises:
146
+ ValidationError: If ``bus`` is a [`BusRef`][qprogram.BusRef] from another schema than the
147
+ one attached to the program.
148
+ """
149
+ self._append(WaitTrigger(bus=bus, duration=duration, port=port))
150
+
151
+ def set_acquisition_threshold(self, bus: str, value: float | Expression) -> None:
152
+ """Append a [`SetAcquisitionThreshold`][qprogram_qblox.SetAcquisitionThreshold] operation.
153
+
154
+ Host-side-only: the platform realizes it as a slow-control parameter write at execution
155
+ time, not as a sequencer instruction. A vendor namespace can expose operations whose effect
156
+ is entirely off-sequencer, and the platform decides at execution time how to realize each
157
+ one.
158
+
159
+ Args:
160
+ bus (str): Readout bus whose discrimination threshold to set.
161
+ value (float | Expression): Threshold, in volts after integration. Accepts an
162
+ [`Expression`][qprogram.Expression] so an enclosing loop can sweep it.
163
+
164
+ Raises:
165
+ ValidationError: If ``bus`` is a [`BusRef`][qprogram.BusRef] from another schema than the
166
+ one attached to the program.
167
+ """
168
+ self._append(SetAcquisitionThreshold(bus=bus, value=value))
169
+
170
+ def set_acquisition_rotation(self, bus: str, angle: float | Expression) -> None:
171
+ """Append a [`SetAcquisitionRotation`][qprogram_qblox.SetAcquisitionRotation] operation.
172
+
173
+ The companion to `set_acquisition_threshold`: the integrated IQ point is rotated by
174
+ ``angle`` before the comparison against the threshold, so the two populations separate along
175
+ one axis. Host-side-only as well, a parameter write rather than a sequencer instruction.
176
+
177
+ Args:
178
+ bus (str): Readout bus whose acquisition rotation to set.
179
+ angle (float | Expression): Rotation angle in radians, the unit convention of core
180
+ [`set_phase`][qprogram.QProgram.set_phase]. Accepts an [`Expression`][qprogram.Expression] so an
181
+ enclosing loop can sweep it, which is how it is normally calibrated.
182
+
183
+ Raises:
184
+ ValidationError: If ``bus`` is a [`BusRef`][qprogram.BusRef] from another schema than the
185
+ one attached to the program.
186
+ """
187
+ self._append(SetAcquisitionRotation(bus=bus, angle=angle))
@@ -0,0 +1,233 @@
1
+ # Copyright 2026 Qilimanjaro Quantum Tech
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Qblox-specific Operation classes.
15
+
16
+ Each class is a concrete [`Operation`][qprogram.operations.Operation] subclass that lives in the QProgram
17
+ AST. They are the data nodes: typed attributes plus the capability tokens they require, serialized
18
+ to and from ``.qp`` by the vendor registry.
19
+
20
+ They span both kinds of vendor operation. [`Acquire`][qprogram_qblox.Acquire],
21
+ [`SetMarkers`][qprogram_qblox.SetMarkers], [`SetTrigger`][qprogram_qblox.SetTrigger] and
22
+ [`WaitTrigger`][qprogram_qblox.WaitTrigger] each map to one sequencer instruction;
23
+ [`SetAcquisitionThreshold`][qprogram_qblox.SetAcquisitionThreshold] and
24
+ [`SetAcquisitionRotation`][qprogram_qblox.SetAcquisitionRotation] map to no sequencer instruction at all and are
25
+ realized as slow-control parameter writes. The AST draws no distinction between the two, and neither does the ``.qp``
26
+ format.
27
+
28
+ A class declares `BUS_ATTRS` and
29
+ `WAVEFORM_ATTRS` only where its data shape differs from the
30
+ ``Operation`` defaults. The base class's introspection methods (``variables``, ``buses``,
31
+ ``waveforms``, ``walk``) read those declarations, so no subclass here overrides them.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from typing import TYPE_CHECKING, ClassVar
37
+
38
+ from qprogram.operations.operation import MeasurementField, MeasurementOperation, Operation, normalize_fields
39
+
40
+ if TYPE_CHECKING:
41
+ from collections.abc import Iterable
42
+
43
+ from qprogram.result import MeasurementHandle
44
+ from qprogram.variable import Expression
45
+ from qprogram.waveforms.waveform import IQWaveform
46
+
47
+
48
+ class Acquire(MeasurementOperation):
49
+ """An acquisition on a readout bus, with no readout pulse of its own.
50
+
51
+ Where core ``measure`` plays a readout pulse and integrates the response, ``acquire`` only
52
+ integrates, which is what a program wants when the pulse is driven separately. It is a
53
+ [`MeasurementOperation`][qprogram.operations.operation.MeasurementOperation] like ``measure``, so it takes part
54
+ in the program's per-bus measurement-name counter: an ``acquire`` after a ``measure`` on the same
55
+ qubit picks up the next free name on that qubit.
56
+
57
+ Args:
58
+ bus (str): Readout bus to acquire on.
59
+ weights (IQWaveform | str): Integration weights, either a concrete
60
+ [`IQWaveform`][qprogram.waveforms.IQWaveform] or a string alias resolved later by
61
+ [`with_waveforms`][qprogram.QProgram.with_waveforms].
62
+ handle (MeasurementHandle): The canonical [`MeasurementHandle`][qprogram.MeasurementHandle] for this
63
+ acquisition. See [`Measure`][qprogram.operations.Measure] for what the runtime writes onto
64
+ it and who reads it.
65
+ fields (Iterable[MeasurementField]): Which measurement fields the platform produces, as an
66
+ iterable of [`MeasurementField`][qprogram.MeasurementField] members. Default
67
+ ``(MeasurementField.IQ,)``. See [`Measure`][qprogram.operations.Measure] for the full
68
+ description. Stored canonically ordered and deduplicated by
69
+ [`normalize_fields`][qprogram.operations.operation.normalize_fields].
70
+
71
+ Raises:
72
+ ValidationError: If ``fields`` is a bare string, is not iterable, requests no field at all,
73
+ or names a field that is not registered.
74
+ """
75
+
76
+ WAVEFORM_ATTRS: ClassVar[tuple[str, ...]] = ("weights",)
77
+
78
+ def __init__(
79
+ self,
80
+ bus: str,
81
+ weights: IQWaveform | str,
82
+ handle: MeasurementHandle,
83
+ fields: Iterable[MeasurementField] = (MeasurementField.IQ,),
84
+ ) -> None:
85
+ self.bus = bus
86
+ self.weights = weights
87
+ self.handle = handle
88
+ self.fields: tuple[str, ...] = normalize_fields(fields)
89
+
90
+ def required_capabilities(self) -> set[str]:
91
+ """Return ``vendor.qblox.acquire`` plus the weights and requested-field tokens.
92
+
93
+ ``waveform.iq`` is always required, since an acquisition integrates an IQ pair. String
94
+ weights contribute ``waveform.alias``; concrete weights contribute the per-class token from
95
+ [`qprogram.protocol.waveform_token`][] when their class is registered. The
96
+ ``measure.fields.<name>`` tokens come from
97
+ [`required_capabilities`][qprogram.operations.operation.MeasurementOperation.required_capabilities].
98
+ """
99
+ from qprogram.protocol import waveform_token # ruff: ignore[import-outside-top-level]
100
+
101
+ caps = super().required_capabilities() | {"vendor.qblox.acquire", "waveform.iq"}
102
+ if isinstance(self.weights, str):
103
+ caps.add("waveform.alias")
104
+ else:
105
+ tok = waveform_token(self.weights)
106
+ if tok is not None:
107
+ caps.add(tok)
108
+ return caps
109
+
110
+
111
+ class SetMarkers(Operation):
112
+ """A new 4-bit marker output mask on a qblox sequencer.
113
+
114
+ Args:
115
+ bus (str): Bus whose marker outputs to drive.
116
+ mask (str): Four characters of ``0`` and ``1``, one per marker line. ``"0001"`` enables
117
+ marker 1.
118
+ """
119
+
120
+ def __init__(self, bus: str, mask: str) -> None:
121
+ self.bus = bus
122
+ self.mask = mask
123
+
124
+ def required_capabilities(self) -> set[str]:
125
+ """Return ``vendor.qblox.set_markers``, the operation's identity token."""
126
+ return {"vendor.qblox.set_markers"}
127
+
128
+
129
+ class SetTrigger(Operation):
130
+ """A trigger output configuration on a qblox sequencer.
131
+
132
+ Args:
133
+ bus (str): Bus whose trigger outputs to arm.
134
+ duration (int): Trigger-active duration in nanoseconds.
135
+ outputs (list[int] | int | None): Trigger output indices to arm, one index or a list of
136
+ them. ``None`` leaves the selection to the platform.
137
+ position (str): Point in the operation at which the trigger fires, either ``"start"`` or
138
+ ``"end"``. Default ``"start"``.
139
+ """
140
+
141
+ def __init__(
142
+ self,
143
+ bus: str,
144
+ duration: int,
145
+ outputs: list[int] | int | None = None,
146
+ position: str = "start",
147
+ ) -> None:
148
+ self.bus = bus
149
+ self.duration = duration
150
+ self.outputs = outputs
151
+ self.position = position
152
+
153
+ def required_capabilities(self) -> set[str]:
154
+ """Return ``vendor.qblox.set_trigger``, the operation's identity token."""
155
+ return {"vendor.qblox.set_trigger"}
156
+
157
+
158
+ class WaitTrigger(Operation):
159
+ """A wait for an external trigger on a qblox sequencer.
160
+
161
+ Args:
162
+ bus (str): Bus whose sequencer waits.
163
+ duration (int): Timeout in nanoseconds, after which the sequencer stops waiting.
164
+ port (int | None): Trigger input port to listen on. ``None`` leaves the choice to the
165
+ platform.
166
+ """
167
+
168
+ def __init__(self, bus: str, duration: int, port: int | None = None) -> None:
169
+ self.bus = bus
170
+ self.duration = duration
171
+ self.port = port
172
+
173
+ def required_capabilities(self) -> set[str]:
174
+ """Return ``vendor.qblox.wait_trigger``, the operation's identity token."""
175
+ return {"vendor.qblox.wait_trigger"}
176
+
177
+
178
+ class SetAcquisitionThreshold(Operation):
179
+ """A new qubit-state discrimination threshold on a readout bus.
180
+
181
+ A **host-side-only** vendor operation: the qblox platform realizes it as a slow-control
182
+ parameter write at execution time and emits no sequencer instruction. Vendor operations do not
183
+ have to map onto sequencer instructions at all: an extension may expose any operation whose
184
+ execution its platform knows how to interpret, be that a sequencer command, a parameter write,
185
+ or a multi-step orchestration.
186
+
187
+ Args:
188
+ bus (str): Readout bus whose discrimination threshold to set.
189
+ value (float | Expression): Threshold, in volts after integration. Accepts an
190
+ [`Expression`][qprogram.Expression] for sweeps.
191
+ """
192
+
193
+ def __init__(self, bus: str, value: float | Expression) -> None:
194
+ self.bus = bus
195
+ self.value = value
196
+
197
+ def required_capabilities(self) -> set[str]:
198
+ """Return ``vendor.qblox.set_acquisition_threshold`` plus the ``value`` expression tokens."""
199
+ from qprogram.protocol import expression_tokens # ruff: ignore[import-outside-top-level]
200
+
201
+ return {"vendor.qblox.set_acquisition_threshold"} | expression_tokens(self.value)
202
+
203
+
204
+ class SetAcquisitionRotation(Operation):
205
+ """A new acquisition rotation angle on a readout bus.
206
+
207
+ The other half of qblox's thresholded acquisition, and the sibling of
208
+ [`SetAcquisitionThreshold`][qprogram_qblox.SetAcquisitionThreshold]: the integrated IQ point is rotated by this
209
+ angle so that the ground and excited populations separate along a single axis, and only then compared against the
210
+ threshold. Setting one without the other is legal, since they are independent parameters, but a calibrated
211
+ discrimination usually writes both.
212
+
213
+ Host-side-only like the threshold: a slow-control parameter write at execution time, not a
214
+ sequencer instruction.
215
+
216
+ Args:
217
+ bus (str): Readout bus whose acquisition rotation to set.
218
+ angle (float | Expression): Rotation angle in **radians**, the unit convention of the core
219
+ phase operations ([`SetPhase`][qprogram.operations.SetPhase]). Accepts an
220
+ [`Expression`][qprogram.Expression] for sweeps, which is the usual way to calibrate it. Values
221
+ outside ``[0, 2π)`` are the platform's to normalize or reject, not this node's: a swept
222
+ angle has no literal value to check at build time.
223
+ """
224
+
225
+ def __init__(self, bus: str, angle: float | Expression) -> None:
226
+ self.bus = bus
227
+ self.angle = angle
228
+
229
+ def required_capabilities(self) -> set[str]:
230
+ """Return ``vendor.qblox.set_acquisition_rotation`` plus the ``angle`` expression tokens."""
231
+ from qprogram.protocol import expression_tokens # ruff: ignore[import-outside-top-level]
232
+
233
+ return {"vendor.qblox.set_acquisition_rotation"} | expression_tokens(self.angle)
@@ -0,0 +1,271 @@
1
+ # Copyright 2026 Qilimanjaro Quantum Tech
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Capability profile bundles for the Qblox vendor extension.
15
+
16
+ Defines [`QBLOX_DEFAULT_V1`][qprogram_qblox.QBLOX_DEFAULT_V1], the bus-level profile describing what a qblox-driven bus
17
+ can do: the pulse, timing and parameter operations, every waveform class the sequencers render, the measurement fields a
18
+ readout produces, and the ``vendor.qblox.*`` operations. Qblox drives its sequencers in real time, so a platform fills a
19
+ bus slot's ``rt`` half with it; the platform slot is filled by the core-shipped ``qprogram-base-v1``, which carries the
20
+ block, sweep and expression tokens. Together the two give a complete
21
+ [`PlatformCapabilities`][qprogram.PlatformCapabilities] shape.
22
+
23
+ Two predicates travel with the profile:
24
+
25
+ - `_reject_arbitrary_sweep_at_wait_duration`, a hard [`Diagnostic`][qprogram.Diagnostic]. The
26
+ operand register of the qblox wait instruction advances by a fixed step, so a duration swept from
27
+ an arbitrary source fits no execution model qblox can compile, real-time or host-side.
28
+ - `_drag_sigma_in_loop_is_host_only`, a soft [`DomainConstraint`][qprogram.DomainConstraint]. The
29
+ sequencer cannot recompute an [`IQDrag`][qprogram.waveforms.IQDrag] envelope between iterations, but
30
+ the host can dispatch one shot per iteration. The constraint excludes ``"rt"`` alone, so the
31
+ binding loop classifies as ``{host}`` while the ``play`` inside it stays real-time.
32
+
33
+ Registered as a side effect of importing `qprogram_qblox`.
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ from typing import TYPE_CHECKING
39
+
40
+ from qprogram.operations.play import Play
41
+ from qprogram.operations.wait import Wait
42
+ from qprogram.protocol import (
43
+ Diagnostic,
44
+ DomainConstraint,
45
+ Profile,
46
+ ValidationContext,
47
+ register_capability_tokens,
48
+ register_profile,
49
+ )
50
+ from qprogram.variable import Variable
51
+ from qprogram.waveforms.iq_drag import IQDrag
52
+
53
+ # Register vendor-specific capability tokens *before* constructing the profile that names them:
54
+ # Profile.__post_init__ validates that every listed token is in CAPABILITY_REGISTRY, so registration
55
+ # must come first.
56
+ register_capability_tokens(
57
+ "vendor.qblox.acquire",
58
+ "vendor.qblox.set_markers",
59
+ "vendor.qblox.set_trigger",
60
+ "vendor.qblox.wait_trigger",
61
+ "vendor.qblox.set_acquisition_threshold",
62
+ "vendor.qblox.set_acquisition_rotation",
63
+ )
64
+
65
+ if TYPE_CHECKING:
66
+ from collections.abc import Iterable
67
+
68
+ from qprogram.blocks.block import Block
69
+ from qprogram.operations.operation import Operation
70
+
71
+
72
+ def _reject_arbitrary_sweep_at_wait_duration(
73
+ node: Operation | Block,
74
+ ctx: ValidationContext,
75
+ ) -> Iterable[Diagnostic | DomainConstraint]:
76
+ """Reject a ``wait`` whose duration is bound to an arbitrary sweep source.
77
+
78
+ The qblox wait instruction takes one integer cycle count, and the register holding it advances
79
+ by a fixed step, so the hardware can only walk a duration that is an exact ``start + step * i``
80
+ ramp. The predicate asks [`sweep_kind_of`][qprogram.ValidationContext.sweep_kind_of] how the variable at
81
+ `duration` is bound and fires when the binding
82
+ [`Sweep`][qprogram.blocks.Sweep]'s source declares ``KIND = "arbitrary"``:
83
+ [`Values`][qprogram.sweeps.Values], [`Logspace`][qprogram.sweeps.Logspace],
84
+ [`File`][qprogram.sweeps.File], and every combinator around them. A linear source
85
+ ([`Range`][qprogram.sweeps.Range], [`Linspace`][qprogram.sweeps.Linspace]) passes, and so does a
86
+ constant duration, which is not loop-bound at all.
87
+
88
+ Host-side dispatch cannot rescue the combination, since qblox still has to emit the wait
89
+ instruction per shot. That is why this is a [`Diagnostic`][qprogram.Diagnostic] rather than a
90
+ [`DomainConstraint`][qprogram.DomainConstraint].
91
+
92
+ Args:
93
+ node (Operation | Block): The AST node currently being checked.
94
+ ctx (ValidationContext): Validation context, used to look up how the duration variable is
95
+ bound.
96
+
97
+ Yields:
98
+ One ``"qblox.arbitrary-wait-sweep"`` error [`Diagnostic`][qprogram.Diagnostic] when ``node`` is a
99
+ ``wait`` whose duration variable is bound to an arbitrary source. Nothing otherwise.
100
+ """
101
+ if not isinstance(node, Wait):
102
+ return
103
+ if not isinstance(node.duration, Variable):
104
+ return
105
+ if ctx.sweep_kind_of(node.duration) == "arbitrary":
106
+ yield Diagnostic(
107
+ severity="error",
108
+ code="qblox.arbitrary-wait-sweep",
109
+ message=(
110
+ f"Variable {node.duration.id!r} is swept with arbitrary "
111
+ f"values and used at Wait.duration, which qblox does not "
112
+ f"support (the wait instruction needs a linear step). Use "
113
+ f"a linear sweep source (Range / Linspace) instead, or a constant duration."
114
+ ),
115
+ node=node,
116
+ )
117
+
118
+
119
+ def _drag_sigma_in_loop_is_host_only(
120
+ node: Operation | Block,
121
+ ctx: ValidationContext,
122
+ ) -> Iterable[Diagnostic | DomainConstraint]:
123
+ """Restrict the binding loop of a swept ``IQDrag.sigma`` to host-side dispatch.
124
+
125
+ A qblox sequencer re-arms a real-time loop with a new amplitude or duration on its own, but it
126
+ cannot recompute a Drag envelope's ``sigma`` between iterations: the gaussian and its derivative
127
+ are sampled once, at upload. Sweeping ``sigma`` therefore means re-uploading the waveform per
128
+ iteration, which the enclosing loop can only do host-side, one qblox shot at a time. The
129
+ [`Play`][qprogram.operations.Play] itself stays real-time; what changes is how the loop around it
130
+ iterates.
131
+
132
+ The constraint targets the loop returned by [`binding_loop_of`][qprogram.ValidationContext.binding_loop_of],
133
+ not the ``Play``, because that is the node whose iteration mechanism is at stake. The classifier
134
+ subtracts ``"rt"`` from the loop's support and dispatches the ``Play`` as one real-time shot per
135
+ host-side iteration. A ``sigma`` that no loop binds is a constant at upload time and is left
136
+ alone.
137
+
138
+ Args:
139
+ node (Operation | Block): The AST node currently being checked.
140
+ ctx (ValidationContext): Validation context, used to find the loop that binds ``sigma``.
141
+
142
+ Yields:
143
+ One [`DomainConstraint`][qprogram.DomainConstraint] excluding ``"rt"`` from the binding loop, when
144
+ ``node`` is a ``play`` of an [`IQDrag`][qprogram.waveforms.IQDrag] whose ``sigma`` is a
145
+ loop-bound variable. Nothing otherwise.
146
+ """
147
+ if not isinstance(node, Play) or not isinstance(node.waveform, IQDrag):
148
+ return
149
+ sigma = node.waveform.sigma
150
+ if not isinstance(sigma, Variable):
151
+ return
152
+ binding_loop = ctx.binding_loop_of(sigma)
153
+ if binding_loop is None:
154
+ return
155
+ yield DomainConstraint(
156
+ node=binding_loop,
157
+ exclude=frozenset({"rt"}),
158
+ reason=(
159
+ f"Variable {sigma.id!r} sweeps IQDrag.sigma in a contained Play, which qblox "
160
+ f"cannot real-time-update; the loop dispatches per shot host-side instead."
161
+ ),
162
+ )
163
+
164
+
165
+ _BUS_OPS: frozenset[str] = frozenset(
166
+ {
167
+ "op.play",
168
+ "op.measure",
169
+ "op.wait",
170
+ "op.sync",
171
+ "op.set_frequency",
172
+ "op.set_phase",
173
+ "op.set_gain",
174
+ "op.reset_phase",
175
+ "op.set_offset",
176
+ },
177
+ )
178
+ """Core operations a qblox sequencer executes.
179
+
180
+ Each of them targets a bus, so its token belongs on this bus-level profile rather than on the
181
+ platform slot: the validator routes a bus-touching operation to the slot its bus resolves to.
182
+ """
183
+
184
+ _WAVEFORMS: frozenset[str] = frozenset(
185
+ {
186
+ "waveform.single",
187
+ "waveform.iq",
188
+ "waveform.alias",
189
+ "waveform.arbitrary",
190
+ "waveform.chained",
191
+ "waveform.flat_top",
192
+ "waveform.gaussian",
193
+ "waveform.gaussian_drag_correction",
194
+ "waveform.ramp",
195
+ "waveform.snz",
196
+ "waveform.square",
197
+ "waveform.iq_drag",
198
+ "waveform.iq_pair",
199
+ },
200
+ )
201
+ """Waveform tokens the qblox sequencers render.
202
+
203
+ Two levels, both bus-level because a waveform only reaches the hardware through a bus: the channel
204
+ kind (``waveform.single`` and ``waveform.iq``, since qblox drives both single-channel and IQ buses),
205
+ ``waveform.alias`` for a name a [`WaveformLibrary`][qprogram.WaveformLibrary] resolves later, and one per-class
206
+ token for each envelope the sequencers can sample.
207
+ """
208
+
209
+ _FIELDS: frozenset[str] = frozenset(
210
+ {
211
+ "measure.fields.iq",
212
+ "measure.fields.raw",
213
+ "measure.fields.state",
214
+ },
215
+ )
216
+ """Measurement fields a qblox readout produces.
217
+
218
+ All three core members of [`MeasurementField`][qprogram.MeasurementField]: the integrated IQ point, the raw ADC
219
+ trace, and the thresholded state. [`Measure`][qprogram.operations.Measure] and
220
+ [`acquire`][qprogram_qblox.QbloxNamespace.acquire] attach a ``measure.fields.<name>`` token per
221
+ requested field and both target a bus, which is why the tokens are bus-level.
222
+ """
223
+
224
+ _VENDOR: frozenset[str] = frozenset(
225
+ {
226
+ "vendor.qblox.acquire",
227
+ "vendor.qblox.set_markers",
228
+ "vendor.qblox.set_trigger",
229
+ "vendor.qblox.wait_trigger",
230
+ "vendor.qblox.set_acquisition_threshold",
231
+ "vendor.qblox.set_acquisition_rotation",
232
+ },
233
+ )
234
+ """The ``vendor.qblox.*`` operation tokens.
235
+
236
+ Every operation this package ships carries a ``bus``, so all six route to the bus slot. A bus-less
237
+ vendor operation would need its token on the platform slot instead.
238
+ """
239
+
240
+
241
+ QBLOX_DEFAULT_V1 = Profile(
242
+ name="qblox-default-v1",
243
+ version=(0, 1, 0),
244
+ extends=None,
245
+ capabilities=_BUS_OPS | _WAVEFORMS | _FIELDS | _VENDOR,
246
+ limits={"min_wait_duration_ns": 4},
247
+ predicates=(
248
+ _reject_arbitrary_sweep_at_wait_duration,
249
+ _drag_sigma_in_loop_is_host_only,
250
+ ),
251
+ vendor_versions={"qblox": (0, 1, 0)},
252
+ )
253
+ """The default Qblox bus-level capability profile.
254
+
255
+ Holds every token a qblox-driven bus accepts (`_BUS_OPS`, `_WAVEFORMS`, `_FIELDS`
256
+ and `_VENDOR`), the ``min_wait_duration_ns`` floor of the wait instruction, and the two
257
+ predicates above. It carries no ``block.*``, ``sweep.*`` or ``expr.*`` token: those route to the
258
+ platform slot, which a qblox platform fills from ``qprogram-base-v1``.
259
+
260
+ Reach it by name with
261
+ ``CompilerCapabilities.from_profile("qblox-default-v1", limit_overrides=...)``, or extend it with a
262
+ profile of your own that declares ``extends="qblox-default-v1"`` and lists only what differs.
263
+ """
264
+
265
+
266
+ def _register() -> None:
267
+ """Idempotently register [`QBLOX_DEFAULT_V1`][qprogram_qblox.QBLOX_DEFAULT_V1] on the global profile registry."""
268
+ register_profile(QBLOX_DEFAULT_V1)
269
+
270
+
271
+ __all__ = ["QBLOX_DEFAULT_V1"]
File without changes
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.5
2
+ Name: qprogram-qblox
3
+ Version: 0.1.0
4
+ Summary: Qblox vendor extensions for the QProgram pulse-level quantum programming DSL.
5
+ Project-URL: Homepage, https://github.com/qilimanjaro-tech/qprogram-qblox
6
+ Project-URL: Documentation, https://qilimanjaro-tech.github.io/qprogram-qblox/
7
+ Project-URL: Source, https://github.com/qilimanjaro-tech/qprogram-qblox
8
+ Project-URL: Issues, https://github.com/qilimanjaro-tech/qprogram-qblox/issues
9
+ Author-email: Qilimanjaro Quantum Tech <info@qilimanjaro.tech>
10
+ License-Expression: Apache-2.0
11
+ License-File: LICENSE
12
+ Keywords: dsl,pulse programming,qblox,qilimanjaro,quantum computing,quantum control
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Operating System :: Microsoft :: Windows
19
+ Classifier: Operating System :: POSIX :: Linux
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Scientific/Engineering
26
+ Classifier: Topic :: Scientific/Engineering :: Physics
27
+ Classifier: Topic :: Scientific/Engineering :: Quantum Computing
28
+ Classifier: Typing :: Typed
29
+ Requires-Python: >=3.11
30
+ Requires-Dist: qprogram>=0.1.0
31
+ Description-Content-Type: text/markdown
32
+
33
+ # QProgram Qblox
34
+
35
+ [![Tests](https://github.com/qilimanjaro-tech/qprogram-qblox/actions/workflows/tests.yml/badge.svg)](https://github.com/qilimanjaro-tech/qprogram-qblox/actions/workflows/tests.yml)
36
+ [![Code Quality](https://github.com/qilimanjaro-tech/qprogram-qblox/actions/workflows/code_quality.yml/badge.svg)](https://github.com/qilimanjaro-tech/qprogram-qblox/actions/workflows/code_quality.yml)
37
+ [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://www.python.org/)
38
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE)
39
+
40
+ Qblox extensions for [QProgram](https://github.com/qilimanjaro-tech/qprogram), a Python DSL for
41
+ pulse-level quantum experiments.
42
+
43
+ The core DSL knows nothing about any instrument. This package teaches it about the Qblox cluster.
44
+ It adds six operations that the portable language does not cover, a capability profile that says
45
+ what QCM and QRM sequencers accept, and `.qp` serialization for everything it adds. Importing the
46
+ package is the whole activation step.
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ pip install qprogram-qblox
52
+ ```
53
+
54
+ The core `qprogram` package installs with it.
55
+
56
+ ## A first program
57
+
58
+ Calibrate the acquisition rotation angle of a readout bus: sweep the angle, discriminate the
59
+ qubit state at each point, and average the outcome.
60
+
61
+ ```python
62
+ import math
63
+
64
+ import qprogram as qp
65
+ from qprogram.buses import BusSchema
66
+ from qprogram.waveforms import IQDrag, IQPair, Square
67
+
68
+ from qprogram_qblox import QProgram # qprogram.QProgram with a typed .qblox namespace
69
+
70
+ schema = BusSchema.transmon()
71
+ q = schema.q
72
+
73
+ program = QProgram(label="rotation_calibration", schema=schema)
74
+ angle = program.variable("angle", units="rad")
75
+
76
+ with program.average(shots=1000):
77
+ with program.sweep(angle).from_linspace(0.0, math.pi, 21):
78
+ program.qblox.set_acquisition_rotation(q[0].readout, angle)
79
+ program.play(q[0].drive, "pi_pulse")
80
+ program.sync()
81
+ m0 = program.qblox.acquire(q[0].readout, "weights", fields=(qp.MeasurementField.STATE,))
82
+
83
+ # Bind calibrated waveforms at the very end; the program itself only names them.
84
+ resolved = program.with_waveforms(
85
+ {
86
+ "pi_pulse": IQDrag(amplitude=0.5, duration=40, sigma=8, beta=0.1),
87
+ "weights": IQPair(Square(1.0, 2000), Square(1.0, 2000)),
88
+ }
89
+ )
90
+
91
+ result = qp.simulate(resolved)
92
+ population = result.get(m0, field=qp.MeasurementField.STATE)
93
+ print(population.dims, population.shape) # ('angle',) (21,)
94
+ ```
95
+
96
+ `qp.simulate` is the reference software executor that ships with the core package. It runs the
97
+ qblox operations generically: `qblox.acquire` produces measurement records, the rest have no
98
+ effect on the simulated outcome. A Qblox platform is a drop-in for the same call and lowers each
99
+ operation onto real sequencers.
100
+
101
+ ## What you get
102
+
103
+ - **Six operations under `program.qblox`.** `acquire` reads a bus without playing a readout
104
+ pulse. `set_markers` and `set_trigger` drive the digital outputs, `wait_trigger` waits on a
105
+ digital input. `set_acquisition_threshold` and `set_acquisition_rotation` configure
106
+ thresholded acquisition, and take effect off the sequencer as slow-control parameter writes
107
+ at execution time.
108
+ - **Typed or dynamic access.** `qprogram_qblox.QProgram` has `.qblox` typed for autocomplete,
109
+ `QbloxMixin` composes with other vendor mixins, and the plain `qprogram.QProgram` gets the
110
+ same namespace at runtime once this package is imported.
111
+ - **A capability profile.** `QBLOX_DEFAULT_V1` declares the operations, waveforms, measurement
112
+ fields, and limits of a qblox-driven bus, plus the two constraints the hardware imposes: an
113
+ arbitrary-valued sweep cannot drive a wait duration, and sweeping an `IQDrag` sigma forces its
114
+ loop to iterate host-side.
115
+ - **Round-tripping `.qp` files.** Every operation serializes as `qblox.<name> <args>` and reloads
116
+ to a structurally equal program. Files carry `require qblox 0.1`, which the parser checks
117
+ against the installed version.
118
+ - **Auto-activation on load.** `qprogram.load("file.qp")` imports this package on demand when the
119
+ file requires the `qblox` vendor, so a reader never has to know which extensions a file uses.
120
+
121
+ ## Documentation
122
+
123
+ Full documentation, including the operation reference, the capability profile, and the generated
124
+ API reference, lives at <https://qilimanjaro-tech.github.io/qprogram-qblox/>.
125
+
126
+ ## Development
127
+
128
+ The project uses [uv](https://docs.astral.sh/uv/).
129
+
130
+ ```bash
131
+ git clone https://github.com/qilimanjaro-tech/qprogram-qblox
132
+ cd qprogram-qblox
133
+
134
+ uv sync --group dev # create .venv and install the package plus dev tools
135
+ uv run pytest # run the test suite
136
+ uv run ruff check . # lint
137
+ uv run ruff format . # format
138
+ uv run ty check # type-check
139
+ uv run --group docs zensical serve # preview the documentation
140
+ ```
141
+
142
+ ## License
143
+
144
+ Apache License 2.0 - see [LICENSE](LICENSE).
@@ -0,0 +1,11 @@
1
+ qprogram_qblox/__init__.py,sha256=g0LFTyI9OhT42BTPMhRy5GP8Fkfa_Me6vCFv3qveFgw,6532
2
+ qprogram_qblox/mixin.py,sha256=UHVrXkpXpu5yV0ihW1wDW9eJXLh9b_xSHVIGLahUsuM,2975
3
+ qprogram_qblox/namespace.py,sha256=U7jevfYu2LmEaQV2mhbXjAd_s8-fiGCYUZi0JbCNtsg,8515
4
+ qprogram_qblox/operations.py,sha256=DtWXkJ46WpKZp6eroxThUlkEbRftPopUOalMlKHQrkE,10424
5
+ qprogram_qblox/profiles.py,sha256=cfLjnKB-AeucFA-AFdrO72qckjpz83oJeHCcHwWNTGs,11160
6
+ qprogram_qblox/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ qprogram_qblox-0.1.0.dist-info/METADATA,sha256=IjJ2H-G2nGnPlg-HVAR8Xt39e4in5Aj2tFFxGOXEL5o,6443
8
+ qprogram_qblox-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
9
+ qprogram_qblox-0.1.0.dist-info/entry_points.txt,sha256=9-k7fH4cck1lXJ9j8j-aebXlzGtHyz3pFovFsEZ3jWU,42
10
+ qprogram_qblox-0.1.0.dist-info/licenses/LICENSE,sha256=SSd4NLHKeN_X9uu9A5TnY3vpro7w7I9ZduIIsjtIBlo,11354
11
+ qprogram_qblox-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [qprogram.vendors]
2
+ qblox = qprogram_qblox
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2025 Qilimanjaro Quantum Tech
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.