-
Notifications
You must be signed in to change notification settings - Fork 15
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
Acquisition #970
Acquisition #970
Conversation
f57a57b
to
c3685f8
Compare
d61323e
to
a63f638
Compare
c3685f8
to
b07b2ec
Compare
8b0ad6f
to
914f9a4
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## 0.2 #970 +/- ##
==========================================
+ Coverage 41.91% 42.67% +0.76%
==========================================
Files 79 80 +1
Lines 5464 5537 +73
==========================================
+ Hits 2290 2363 +73
Misses 3174 3174
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
9ac2d23
to
f9d4daf
Compare
5dcb64d
to
8ba8c04
Compare
f9d4daf
to
2171caa
Compare
8ba8c04
to
3a8b20b
Compare
Analogously to #965 (comment), this may become handy: import sys
import json
from pathlib import Path
from typing import Optional
path = Path(sys.argv[1])
run = json.loads(path.read_text())
def upgrade(
gate: str, seq: Optional[list[tuple[str, dict]]]
) -> Optional[list[tuple[str, dict]]]:
"""Upgrade sequence layout.
From https://github.com/qiboteam/qibolab/pull/971 to
https://github.com/qiboteam/qibolab/pull/970
"""
new = []
if seq is None:
return None
for ch, pulse in seq:
if "amplitude" in pulse:
pulse["kind"] = "pulse"
elif "phase" in pulse:
pulse["kind"] = "virtualz"
else:
pulse["kind"] = "delay"
new.append((ch, pulse))
if gate == "MZ":
new.append(
(
ch.replace("probe", "acquisition"),
{"kind": "acquisition", "duration": pulse["duration"]},
)
)
return new
for section in run["native_gates"].values():
for qubit, gates in section.items():
for gate, sequence in gates.items():
gates[gate] = upgrade(gate, sequence)
path.write_text(json.dumps(run, indent=2)) |
Btw, just a fun fact: the Python validation in Now they are taking the double wrt to #971:
In practice, it's pretty much irrelevant. And the easiest way to speed up the tests is to stop testing the full product space of options in However, it's a remarkable performance drop. I'm tempted to load sequences with channels as |
1e60151
to
0ac2260
Compare
I will rebase asap, but other than that is ready. |
The qibolab/src/qibolab/pulses/pulse.py Lines 42 to 53 in 19dd810
it's just overwriting .relative_phase , whatever the user has given. While a 0.0 default is already present, thus is sufficient to avoid specifying it...
Shall we remove it? |
Replacing them with acquisitions
19dd810
to
1940ba2
Compare
Btw, I'm not fully convinced regarding names (e.g. |
duration: float | ||
"""Duration in ns.""" |
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.
If this duration is different than the corresponding probe, I am not sure how I would handle it in the instrument. At least for QM, the only way that I am aware to control this duration is the smearing
, and maybe also the time of flight, however the latter works like a delay, rather than a different duration. However, we are already setting these parameters as part of the acquisition config, so wouldn't providing a different duration here duplicate them and potentially even create conflicts? For example a different smearing is needed to force the required acquisition duration, than the one provided in the config.
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.
The idea is to use it for instruments where they are truly independent, event per event.
In QM, you should use the _Readout
object, generated by the PulseSequence.as_readouts
property.
For the time being, I'm not validating that the probe and acquisition have the same duration, but I'm implicitly assuming so. I could even add an explicit validation in the _Readout
object, or we can leave this up to the driver (either validate, or neglect one of the two).
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.
Using _Readout
will ignore the duration
of the probe pulse and use the acquisition one. This may not be ideal without throwing a warning/error if the two durations are different, to tell the users that we are not actually doing this. I have not tried, but it could be that an error will be raised by QM because the waveform will be generated using the probe, while the length (duration) will be read from the acquisition and if the two are not compatible the instrument will probably not like it. This may not be true for rectangular pulses, though, so it may be better to catch this earlier, either in as_readouts
or in the driver.
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.
Warnings are pretty useless in practice (though we could think more about logging), they would simply be ignored. If someone notices a problem, there are high chances we'll be contacted before the user reading the warning...
We could raise manually, either in as_readouts
/_Readout
or in the driver. In the first case, it would be imposed to every as_readouts
user. Though, it is likely that you want it, if you're using as_readouts
.
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.
Generally, I have the impression that separating acquisition makes the life of everyone harder:
- qibolab developers have to support it in the drivers and maintain nasty functions like
as_readouts
, - users (or routine authors) have to add more pulses in the sequence to achieve the same thing,
- platform authors have to add an additional pulse in the native measurement, essentially providing only an additional duration, that may even be ignored by some instruments.
and all this needs to be documented and well explained to users.
It is not clear to me what we get in return, other than the ability to acquire for different duration than the probe or potentially add some delay between the two, which was generally already possible with time of flight/smearing, and the ability to play acquisition without probe which is not clear which instruments can support it and if it is relevant for any practical applications, at least near term.
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.
In general, you could even acquire without any probe at all. This can be useful, not only for improving the measurement, but even for designing calibration experiments (e.g. for acquiring pulses in loop-back, and calibrating what it is the transmission baseline, and you would classify as 1 - but also to characterize physical units with external instruments, or among them).
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.
It may not be that useful during qubit operations (for which smearing/time of flight could be enough) but it may be strictly required for a different usage of the devices (i.e. their isolated calibration).
I hoped that smearing/time of flight were doing the exact same thing, just with a more convenient interface for experiments (as a rotation in the space of parameters). If there are instruments for which this is the case, so much the better, it's just a translation. Otherwise, they will fail whatever they can't do.
However, I see the acquisition as a more native operation (I'd argue it is), since the instrument DAC and ADC are different discrete elements, and there should be no bond that makes them operating at the same time. Other than the (programmable) logic built on top.
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.
In any case, if it's unbearable, we could even expose the Readout
as part of the interface. If you don't need the split, you could use that. Possibly, with a simpler constructor (such that you don't have to specify both the events). But it doesn't seem that complicate, to me, defining the acquisition as a separate event (and possibly even avoid time of flight/smearing at all in Qibolab interface, compiling them within the driver).
Even better, but further in the future, we could avoid compiling the measurement completely, just keeping it as an atom in Qibolab representation (even in sequences), since most of the drivers will register the measurement parameters, and then just execute the measurement (which is exactly what smearing and time of flight are doing).
Yes, is this used anywhere? |
Only in the tests |
@stavros11 pay attention that these are more or less two features in one PR (related, but up to a point). One consists of the new channel identifiers, the other one of the automated readout collection. |
class ChannelId(Model): | ||
"""Unique identifier for a channel.""" | ||
|
||
qubit: QubitId |
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.
Isn't this a bit cyclic? We have, Qubit
-> Channel
-> ChannelId
-> QubitId
. Of course it is not cyclic in terms of objects because QubitId != Qubit
, but at least conceptually wise it kind of is.
At this stage, I am not sure if we even need Qubit
objects. We could just pass the logical channels directly to the platform and keep them in a dict[str, Channel]
. The stricter would be dict[ChannelId, Channel]
, however that may be less user friendly when creating sequences, as
sequence.append((platform.channels["0/drive"], Pulse(...)))
is simpler than
sequence.append((platform.channels[ChannelId(...)], Pulse(...)))
even though we could also alleviate this by defining an appropriate getter in the Platform
.
On the other hand, I am not sure if making channels depend on qubits (in this case QubitId
) is a good idea. I understand here you are trying to standardize channel names. However, in general, channels are a more primitive idea than qubits, as in qubits need channels to be controlled, not the other way around. Channels could survive without qubits, potentially in non-quantum computing applications.
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.
Isn't this a bit cyclic? We have,
Qubit
->Channel
->ChannelId
->QubitId
. Of course it is not cyclic in terms of objects becauseQubitId != Qubit
, but at least conceptually wise it kind of is.
The problem of your picture is Channel
-> ChannelId
: an object should not be aware of its own identifier, because that's how the other ones are calling you, but you don't really need to call yourself.
On the other hand, I am not sure if making channels depend on qubits (in this case
QubitId
) is a good idea. I understand here you are trying to standardize channel names. However, in general, channels are a more primitive idea than qubits, as in qubits need channels to be controlled, not the other way around. Channels could survive without qubits, potentially in non-quantum computing applications.
True, but Qibolab is mostly about quantum computing.
In any case, the main reason why ChannelId
now contains QubitId
is exactly because Qubit
contains the Channel
: if you have a pulse sequence, you truly want to be able to get a handle on the channel, given a platform
. So, the ChannelId
is a practical path to retrieve the Channel
you need.
If instead we give up on Qubit
as a Channel
collection (and thus on the Qubit
object tout-court, because that's what is left), we don't need to navigate, but just grab the channel from the channels' container (a single platform attribute).
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.
The stricter would be
dict[ChannelId, Channel]
, however that may be less user friendly when creating sequences, assequence.append((platform.channels["0/drive"], Pulse(...)))is simpler than
sequence.append((platform.channels[ChannelId(...)], Pulse(...)))even though we could also alleviate this by defining an appropriate getter in the
Platform
.
Here you're a bit unfair, since the current layout is just:
sequence.append((ChannelId(...), Pulse(...)))
And that's better than everything: a pulse sequence should not be bound to a specific platform
, but just a sequence of operations. If you change platform, but you keep the same channel IDs, the instructions should still execute (which is relevant for serialization, when you dump and load the sequence again).
In any case, without the Qubit
, we could definitely also have a simpler ChannelId
, just reminiscent of the role of the channel (i.e. the ChannelType
)[*], while retrieving the qubit association from somewhere else.
In a sense, this is really a leftover: by now, everything is working by named reference, without nesting objects (other than in parameters.json
), and that was the idea of the components configs. But we could treat the actual components in the same way, and just retain the name in the Qubit
s (to answer the question: which is the channel for this qubit's flux?)
[*]: we could drop even the ChannelType
, but this information is instead relevant even in the PulseSequence
, so I'd try to keep it somehow - the other option is to make PulseSequence
a full-blown object, not just a list, and register the channel's roles in there (not sure about which could be an effective interface)
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.
The problem of your picture is
Channel
->ChannelId
: an object should not be aware of its own identifier, because that's how the other ones are calling you, but you don't really need to call yourself.
But isn't the Channel
aware ChannelId
in this PR? Channel.name
is the ChannelId
. The -> in my original message basically means "has a reference to". Qubit
has references to Channel
s, Channel
has reference to its ChannelId
, etc..
In any case, the main reason why
ChannelId
now containsQubitId
is exactly becauseQubit
contains theChannel
: if you have a pulse sequence, you truly want to be able to get a handle on the channel, given aplatform
. So, theChannelId
is a practical path to retrieve theChannel
you need.
I think I don't understand this. How ChannelId
helps retrieving Channel
s? I think the way to retrieve them as it is now is still platform.qubits[qubit_name].drive
, etc. but that was working even before this PR.
If instead we give up on
Qubit
as aChannel
collection (and thus on theQubit
object tout-court, because that's what is left), we don't need to navigate, but just grab the channel from the channels' container (a single platform attribute).
Yes, that's basically my proposal.
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.
But isn't the
Channel
awareChannelId
in this PR?Channel.name
is theChannelId
. The -> in my original message basically means "has a reference to".Qubit
has references toChannel
s,Channel
has reference to itsChannelId
, etc..
Yes, it was not a mistake in your reconstruction, just what I see as problematic.
I think I don't understand this. How
ChannelId
helps retrievingChannel
s? I think the way to retrieve them as it is now is stillplatform.qubits[qubit_name].drive
, etc. but that was working even before this PR.
See below #970 (comment)
In principle, just reading the PulseSequence
you didn't know it was the drive (because names were just whatever str
). So, you had first to find (somehow) that your channel was referred to that qubit, and it was drive. Then you could access.
In practice, you were doing this by keeping a copy
channels: dict[str, QmChannel] |
https://github.com/qiboteam/qibolab_platforms_qrc/blob/4a5bb22dff21ffee0bc2ffca8e136a42003dfda4/qw11qD/platform.py#L71
and never accessing the channels in the
Qubit
objectsqibolab/src/qibolab/platform/platform.py
Line 103 in b49bb42
qubits: QubitMap |
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.
In the previous comments I am mainly talking about how users (mostly qibocal) are supposed to access channels and create sequences, not the driver.
The driver certainly cannot access channels from Qubit
s because it does not have access to Qubit
s and it shouldn't. But the driver also does not care whether a channel is a drive of a particular qubit or something else. It only cares what are the pulses playing on that channel (provided by the sequence) and what is the config (frequencies, offsets, potentially LOs - provided by the configs
dict). It may only need to distinguish acquisition channels from the rest, but I think even this is not necessary now since there is a separate Acquisition
instruction.
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.
Correct. But the driver should also be the only one to care about the Channel
object. So, it makes much more sense to locate the Channel
in the instrument, rather than in the Qubit
in the Platform
.
class ChannelType(str, Enum): | ||
"""Names of channels that belong to a qubit. | ||
|
||
Not all channels are required to operate a qubit. | ||
""" | ||
|
||
PROBE = "probe" | ||
ACQUISITION = "acquisition" | ||
DRIVE = "drive" | ||
DRIVE12 = "drive12" | ||
DRIVE_CROSS = "drive_cross" | ||
FLUX = "flux" |
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.
Following the previous comment, this seems like duplicating Qubit
, that's why I am not sure if we need Qubit
in the end.
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 explained above, it is, exactly because we need to navigate the Qubit
object to access the channel.
You can consider the ChannelId
qubit/chtype
to be the "path":
getattr(platform.qubits[qubit], chtype)
elements = ch.split("/") | ||
# TODO: replace with pattern matching, once py3.9 will be abandoned | ||
if len(elements) > 3: | ||
raise ValueError() | ||
q = TypeAdapter(QubitId).validate_python(elements[0]) | ||
ct = ChannelType(elements[1]) | ||
assert len(elements) == 2 or ct is ChannelType.DRIVE_CROSS | ||
dc = elements[2] if len(elements) == 3 else None | ||
return dict(qubit=q, channel_type=ct, cross=dc) |
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 guess here we are forcing channel names to be f"{qubit_name}/{channel_type}"
. Except the concern in the previous comment (whether channels should depend on qubits) and the fact that it is a bit ZI inspired, it is not necessary bad. The main thing I don't fully like is that we are sort of programming via strings and this results to the usual ugly parser (see PulseShape
and eval
in 0.1, just different kind here).
I guess the proper solution would be to initialize these explicitly
IqChannel(name=ChannelId(0, ChannelType.DRIVE))
however, we still a str
representation to match with the configs
in the JSON, so we would probably end up in the existing one.
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.
The main thing I don't fully like is that we are sort of programming via strings and this results to the usual ugly parser (see
PulseShape
andeval
in 0.1, just different kind here).
They are connected because both related to (de)serialization, but PulseShape
was a much more structured object (a full pulse), while this is pretty simpler (the complicate part is that they are 2 or 3 sections, and that's variable - without drive_cross
it would be much easier).
However, I also don't like it too much, because I'm "manually" parsing, while it could be done automatically by Pydantic (which, unless there are Python functions in the middle, is generating a JSON schema, and then processing the JSON in Rust).
To be fair, drive_cross
is also a bit silly, because is not a channel associated to a single qubit, but more to a pair...
(conceptually, then it will play on a physical line, but it's relevant up to a point whether it is connected to one qubit or the other, it's just addressing the pair)
I'd like much more to have chtype/something
, where something may contain the qubit, but also a qubit pair and what not (qudits, if we go beyond qutrits, and whatever else).
As said above, the association channel <-> qubit
could be kept separate from the channel retrieval.
Moreover, chtype
doesn't even to be that granular, but it's just convenient to have the same information encoded by the channel classes locally in the sequence, i.e. separate DC, modulated, and 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.
As said above, the association
channel <-> qubit
could be kept separate from the channel retrieval.
I also don't understand this. To me it seems that now we cannot even initialize a Channel
without refering to a specific QubitId
.
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.
Assume that we have a single Platform.channels
container (even better, a Instrument.channels
container, and Platform.channels
just being the union of them), we wouldn't need Channel.name
. The name would be just the key in the dictionary, and you only need the ChannelId
to access both:
- the configurations
- the object required to play the pulse
So, there is no need to go from Channel
to ChannelConfig
, as you will always go from ChannelId
to both of them.
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.
In #1001 I'll try to go back to plain strings, without any special meaning.
probe=IqChannel(name=probe, mixer=None, lo=None, acquisition=acquisition), | ||
acquisition=AcquireChannel( | ||
name=acquisition, twpa_pump=pump.name, probe=probe | ||
), |
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.
Another point to note is that currently we have the .acquisition
attribute in the probe channel and the .probe
attribute in the acquisition channel to link one to the other. If you see for example in
FIXME: This is temporary solution to be able to generate acquisition commands on correct channel in drivers, |
these were temporary solutions until the acquisition business is sorted (done in this PR).
These attributes ended up being quite handy for the QM driver, because we need to map both these channels to the same element
in the QM config and having the immediate connection through attributes makes it easier. However, the addition of ChannelId
makes them redundant, since now we can establish the same connection by finding the qubit q = probe.name.qubit
and then looking for f"{q}/acquisition"
in the global channel map. This would require updating the QM driver once again, but that's acceptable as we are finalizing the design.
As said before, I am not sure if it is optimal navigating through channels using the qubit name, however the previous design was not flawless either (it already had the assumption that a probe should have an acquisition), so it should be fine to switch.
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'm actually inclined to merge this PR as is, but add another one on top of the stack, where I'll try to implement some alternative proposals mentioned here, starting from an attempt to remove the Qubit
object.
The other option is that I'll rebase the stack directly on 0.2, and keep this separate. However, ChannelId
s are used a bit in #992, so it's slightly simpler to do it on top (though I will do it on top of #994, not #995, which is unfinished).
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.
Following the last comment, I guess it is convenient to merge this and potentially improve with further PRs on top. @alecandido I am not sure if you would like to open issues with your plan, but if you are already working on it, even just a PR with a checklist could be sufficient.
@stavros11 now all points raised in this PR should be present as items in #1001, and I'll try to simplify the things there, taking a more global perspective (not just fixing some channel accesses or a pulse sequence hacky method, but all at once). It was handy to this in steps, since it was not a simple problem, but a final look should help to find a better "global minimum" (now that we're closer to its region). If you find something that I missed from the discussions, please add it to #1001 OP. I'm merging this. |
Closes #916
ChannelId
qubit/ch_name
andqubit/drive_cross/name
only for cross-resonances channelsSequence.probe_pulses()
Acquire
pulse-like instructionPulse
parsing withAcquisition
detectionPulse
andAcquire