-
Notifications
You must be signed in to change notification settings - Fork 14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Qblox #1088
Draft
alecandido
wants to merge
16
commits into
parse-q1asm
Choose a base branch
from
qblox
base: parse-q1asm
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Qblox #1088
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
d03e4f5
fix: Start laying out the sequence structure
alecandido 8ea0ba3
fix: Add further array serialization
alecandido 41f1d3d
test: Test array serialization as lists
alecandido a9788d3
fix: Complete sequence serialization format
alecandido ebb5291
refactor: Avoid unused and unrequired import
alecandido 6ff195c
fix: Add controller boilerplote
alecandido 95a4a7c
refactor: Add exports to all qblox modules
alecandido d4fea33
fix: Ensure floating point samples in waveforms
alecandido f39cd50
feat: Expose qblox drivers within the public API
alecandido 0d72e49
feat: Add defaults for all optional parameters in the common channels
alecandido 365e2d7
fix: Migrate channel mapper from iqm5q platform
alecandido 1514f00
fix: Scaffold address resolution and sequence processing
alecandido f154af8
build: Upgrade Qblox version to v0.14
alecandido fe7f962
feat: Handle cluster connection and expose modules
alecandido a54f669
feat: Lay out execution workflow
alecandido 4a14942
fix: Avoid reconnections
alecandido File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
from . import ast_ | ||
__all__ = [] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,8 @@ | |
|
||
from ...serialize import Model | ||
|
||
__all__ = [] | ||
|
||
|
||
class Register(Model): | ||
number: int | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
from collections import defaultdict | ||
from functools import cached_property | ||
from typing import Optional | ||
|
||
import qblox_instruments as qblox | ||
from qblox_instruments.qcodes_drivers.module import Module | ||
from qcodes.instrument import find_or_create_instrument | ||
|
||
from qibolab._core.components.configs import Config | ||
from qibolab._core.execution_parameters import ExecutionParameters | ||
from qibolab._core.identifier import ChannelId, Result | ||
from qibolab._core.instruments.abstract import Controller | ||
from qibolab._core.sequence import PulseSequence | ||
from qibolab._core.serialize import Model | ||
from qibolab._core.sweeper import ParallelSweepers | ||
|
||
from .sequence import Sequence | ||
|
||
__all__ = ["Cluster"] | ||
|
||
SAMPLING_RATE = 1 | ||
|
||
|
||
SlotId = int | ||
SequencerId = int | ||
|
||
|
||
class PortAddress(Model): | ||
slot: SlotId | ||
ports: tuple[int, Optional[int]] | ||
input: bool = False | ||
|
||
@classmethod | ||
def from_path(cls, path: str): | ||
"""Load address from :attr:`qibolab.Channel.path`.""" | ||
els = path.split("/") | ||
assert len(els) == 2 | ||
ports = els[1][1:].split("_") | ||
assert 1 <= len(ports) <= 2 | ||
return cls( | ||
slot=int(els[0]), | ||
ports=(int(ports[0]), int(ports[1]) if len(ports) == 2 else None), | ||
input=els[1][0] == "i", | ||
) | ||
|
||
@property | ||
def local_address(self): | ||
"""Physical address within the module. | ||
|
||
It will generate a string in the format ``<direction><channel>`` or | ||
``<direction><I-channel>_<Q-channel>``. | ||
``<direction>`` is ``in`` for a connection between an input and the acquisition | ||
path, ``out`` for a connection from the waveform generator to an output, or | ||
``io`` to do both. | ||
The channels must be integer channel indices. | ||
Only one channel is present for a real mode operating sequencer; two channels | ||
are used for complex mode. | ||
|
||
.. note:: | ||
|
||
Description adapted from | ||
https://docs.qblox.com/en/main/api_reference/cluster.html#qblox_instruments.Cluster.connect_sequencer | ||
""" | ||
direction = "in" if self.input else "out" | ||
channels = ( | ||
str(self.ports[0]) | ||
if self.ports[1] is None | ||
else f"{self.ports[0]}_{self.ports[1]}" | ||
) | ||
return f"{direction}{channels}" | ||
|
||
|
||
class Cluster(Controller): | ||
name: str | ||
"""Device name. | ||
|
||
As described in: | ||
https://docs.qblox.com/en/main/getting_started/setup.html#connecting-to-multiple-instruments | ||
""" | ||
bounds: str = "qblox/bounds" | ||
_cluster: Optional[qblox.Cluster] = None | ||
|
||
@cached_property | ||
def _modules(self) -> dict[SlotId, Module]: | ||
assert self._cluster is not None | ||
return {mod.slot_idx: mod for mod in self._cluster.modules if mod.present()} | ||
|
||
@property | ||
def sampling_rate(self) -> int: | ||
return SAMPLING_RATE | ||
|
||
def connect(self): | ||
if self.is_connected: | ||
return | ||
|
||
self._cluster = find_or_create_instrument( | ||
qblox.Cluster, recreate=True, name=self.name, identifier=self.address | ||
) | ||
self._cluster.reset() | ||
|
||
@property | ||
def is_connected(self) -> bool: | ||
return self._cluster is not None | ||
|
||
def disconnect(self): | ||
assert self._cluster is not None | ||
|
||
for module in self._modules.values(): | ||
module.stop_sequencer() | ||
self._cluster.reset() | ||
self._cluster = None | ||
|
||
def play( | ||
self, | ||
configs: dict[str, Config], | ||
sequences: list[PulseSequence], | ||
options: ExecutionParameters, | ||
sweepers: list[ParallelSweepers], | ||
) -> dict[int, Result]: | ||
results = {} | ||
for ps in sequences: | ||
sequences_ = _prepare(ps, sweepers, options) | ||
sequencers = self._upload(sequences_) | ||
results |= self._execute(sequencers) | ||
return results | ||
|
||
def _upload( | ||
self, sequences: dict[ChannelId, Sequence] | ||
) -> dict[SlotId, dict[ChannelId, SequencerId]]: | ||
channels_by_module = _channels_by_module() | ||
sequencers = defaultdict(dict) | ||
for mod, chs in channels_by_module.items(): | ||
module = self._modules[mod] | ||
assert len(module.sequencers) > len(chs) | ||
# Map sequencers to specific outputs (but first disable all sequencer connections) | ||
module.disconnect_outputs() | ||
for idx, (ch, sequencer) in enumerate(zip(chs, module.sequencers)): | ||
sequencers[mod][ch] = idx | ||
sequencer.sequence(sequences[ch].model_dump()) | ||
# Configure the sequencers to synchronize | ||
sequencer.sync_en(True) | ||
sequencer.connect_sequencer( | ||
PortAddress.from_path(self.channels[ch].path).local_address | ||
) | ||
|
||
return sequencers | ||
|
||
def _execute(self, sequencers: dict[SlotId, dict[ChannelId, SequencerId]]) -> dict: | ||
# TODO: implement | ||
for mod, seqs in sequencers.items(): | ||
module = self._modules[mod] | ||
for seq in seqs.values(): | ||
module.arm_sequencer(seq) | ||
module.start_sequencer() | ||
|
||
return {} | ||
|
||
|
||
def _channels_by_module() -> dict[SlotId, list[ChannelId]]: | ||
# TODO: implement | ||
return {} | ||
|
||
|
||
def _prepare( | ||
sequence: PulseSequence, | ||
sweepers: list[ParallelSweepers], | ||
options: ExecutionParameters, | ||
) -> dict[ChannelId, Sequence]: | ||
sequences = {} | ||
for channel in sequence.channels: | ||
filtered = PulseSequence((ch, pulse) for ch, pulse in sequence if ch == channel) | ||
seq = Sequence.from_pulses(filtered, sweepers, options) | ||
sequences[channel] = seq | ||
|
||
return sequences |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
from collections import defaultdict | ||
from typing import Optional, Union | ||
|
||
from qibolab._core.components.channels import ( | ||
AcquisitionChannel, | ||
Channel, | ||
DcChannel, | ||
IqChannel, | ||
) | ||
|
||
__all__ = ["map_ports"] | ||
|
||
|
||
def _eltype(el: str): | ||
return "qubits" if el[0] == "q" else "couplers" | ||
|
||
|
||
def _chtype(mod: str, input: bool) -> tuple[str, type[Channel]]: | ||
if mod.startswith("qcm_rf"): | ||
return "drive", IqChannel | ||
if mod.startswith("qcm"): | ||
return "flux", DcChannel | ||
if mod.startswith("qrm_rf"): | ||
if input: | ||
return "acquisition", AcquisitionChannel | ||
return "probe", IqChannel | ||
raise ValueError | ||
|
||
|
||
def _port_channels(mod: str, port: Union[int, str], slot: int) -> dict: | ||
if isinstance(port, str) and port.startswith("io"): | ||
return { | ||
"probe": IqChannel(path=f"{slot}/o{port[2:]}"), | ||
"acquisition": AcquisitionChannel(path=f"{slot}/o{port[2:]}"), | ||
} | ||
port = f"o{port}" if isinstance(port, int) else port | ||
name, cls = _chtype(mod, port[0] == "i") | ||
return {name: cls(path=f"{slot}/{port}")} | ||
|
||
|
||
def _premap(cluster: dict): | ||
d = { | ||
"qubits": defaultdict(lambda: defaultdict(dict)), | ||
"couplers": defaultdict(lambda: defaultdict(dict)), | ||
} | ||
|
||
for mod, props in cluster.items(): | ||
slot = props[0] | ||
for port, els in props[1].items(): | ||
for el in els: | ||
nel = el[1:] | ||
d[_eltype(el)][nel] |= _port_channels(mod, port, slot) | ||
|
||
return d | ||
|
||
|
||
def map_ports(cluster: dict, qubits: dict, couplers: Optional[dict] = None) -> dict: | ||
"""Extract channels from compact representation. | ||
|
||
Conventions: | ||
- each item is a module | ||
- the first element of each value is the module's slot ID | ||
- the second element is a map from ports to qubits | ||
- ports | ||
- they are `i{n}` or `o{n}` for the inputs and outputs respectively | ||
- `io{n}` is also allowed, to signal that both are connected (cater for the specific | ||
case of the QRM_RF where there are only one port of each type) | ||
- if it's just an integer, it is intended to be an output (equivalent to `o{n}`) | ||
- values | ||
- list of element names | ||
- they are `q{name}` or `c{name}` for qubits and couplers respectively | ||
- multiple elements are allowed, for multiplexed ports | ||
|
||
.. note:: | ||
|
||
integer qubit names are not allowed | ||
|
||
.. todo:: | ||
|
||
Currently channel types are inferred from the module type, encoded in its name. At | ||
least an override should be allowed (per module, per port, per qubit). | ||
""" | ||
if couplers is None: | ||
couplers = {} | ||
|
||
premap = _premap(cluster) | ||
|
||
channels = {} | ||
for kind, elements in [("qubits", qubits), ("couplers", couplers)]: | ||
for name, el in elements.items(): | ||
for chname, ch in premap[kind][name].items(): | ||
channels[getattr(el, chname)] = ch | ||
if kind == "qubits": | ||
channels[el.acquisition] = channels[el.acquisition].model_copy( | ||
update={"probe": el.probe} | ||
) | ||
return channels |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
from typing import Annotated | ||
|
||
from pydantic import AfterValidator, PlainSerializer, PlainValidator | ||
|
||
from qibolab._core.execution_parameters import ExecutionParameters | ||
from qibolab._core.sequence import PulseSequence | ||
from qibolab._core.serialize import ArrayList, Model | ||
from qibolab._core.sweeper import ParallelSweepers | ||
|
||
from .ast_ import Program | ||
from .parse import parse | ||
|
||
__all__ = [] | ||
|
||
|
||
class Waveform(Model): | ||
data: Annotated[ArrayList, AfterValidator(lambda a: a.astype(float))] | ||
index: int | ||
|
||
|
||
Weight = Waveform | ||
|
||
|
||
class Acquisition(Model): | ||
num_bins: int | ||
index: int | ||
|
||
|
||
class Sequence(Model): | ||
waveforms: dict[str, Waveform] | ||
weights: dict[str, Weight] | ||
acquisitions: dict[str, Acquisition] | ||
program: Annotated[ | ||
Program, PlainSerializer(lambda p: p.asm()), PlainValidator(parse) | ||
] | ||
|
||
@classmethod | ||
def from_pulses( | ||
cls, | ||
sequence: PulseSequence, | ||
sweepers: list[ParallelSweepers], | ||
options: ExecutionParameters, | ||
): | ||
return cls( | ||
waveforms={}, weights={}, acquisitions={}, program=Program(elements=[]) | ||
) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is WIP and I am not sure exactly how it is used, but I would expect drivers to be qubit/coupler agnostic and only play with channels and configs, without caring exactly which qubit each channel controls, but maybe only the channel type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just a support function for writing "simpler" platforms, which I moved from the current sketch of the platform.
The idea is that using this function is fully optional. But the function itself is opinionated, favoring certain layouts (hopefully, the most frequent ones), making it simpler to describe the connections.
An example of its usage is here:
https://github.com/qiboteam/qibolab_platforms_qrc/blob/e4011ded804135d758310082616860bb959d94fd/iqm5q/platform.py#L16-L27
(you can see that this is attempting to mirror the connections that you physically have in the lab, to make the definition simpler when you have that picture - the function is supposed to do the rest - see conventions and caveats in its docstring)
To be fair, the reason why I introduced this is that we're passing
qubits
andcouplers
separately in the platform, and consequently defining them in two separate sequences.However, I refactored many times to improve the representation and implementation, and by now there is a single relic for that separation, i.e.:
qibolab/src/qibolab/_core/instruments/qblox/platform.py
Lines 93 to 96 in 1514f00
All the rest is already performed in the very same way.
So, since it is just post-processing, I should be able to concatenate
qubits
andcouplers
, and just post-processqubits
for the acquisition.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have not looked at the full content of the file in detail so maybe it is indeed useful, however my main concern (and what I don't understand) is why this is under qblox. I would expect the instrument to have no knowledge about
qubits
andcouplers
at all, as thePlatform
never communicates these objects to the instrument.If it is actually useful, it would make more sense to lift this outside the drivers and make it usable by any platform.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As said above, these
qubits
&couplers
issue I should be able to solve, and I will (try to) do it soon.We can consider generalizing it, with some pluggable choices. But for the time being it is tailored to Qblox in a few ways (not infinitely many).
The most outstanding example:
qibolab/src/qibolab/_core/instruments/qblox/platform.py
Lines 19 to 23 in 1514f00
true that we could provide some mappings from module names. But not even all instruments have modules... (e.g. not QICK)
Let's say that I'm using Qblox to experiment the approach. But I'm not aiming to make something more general than that, at first shot.
And keeping it as a fully optional part, even for Qblox, we should be free to just leave it there at some point, and replace with some more general implementation (if general-enough, by reimplementing the current interface making use of the new one, otherwise by keeping both, and deprecating the Qblox-specific one).