-
Notifications
You must be signed in to change notification settings - Fork 17
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
Initial implementation for Keysight QCS #944
base: main
Are you sure you want to change the base?
Changes from all commits
7d70d33
4bca03f
79ef94f
a5fc2fb
145db4e
7f72d61
ecb78cd
3330088
a0110c1
e1bcbf7
03e3498
6e49f85
64e897a
9bad5fb
9b6005a
69bc929
fd29be6
c29d344
78f695b
2511e0e
e92da05
11a7b72
5140abd
38fc84d
c0b9564
3a48f83
50de1d0
402d266
8c279d1
3a29ffa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
from . import components, qcs | ||
from .components import * | ||
from .qcs import * | ||
|
||
__all__ = [] | ||
__all__ += qcs.__all__ | ||
__all__ += components.__all__ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
from . import configs | ||
from .configs import * | ||
|
||
__all__ = [] | ||
__all__ += configs.__all__ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
from typing import Annotated, Literal, Optional | ||
|
||
from pydantic import Field | ||
|
||
from qibolab._core.components import AcquisitionConfig | ||
from qibolab._core.serialize import NdArray | ||
|
||
__all__ = ["QcsAcquisitionConfig"] | ||
|
||
|
||
class QcsAcquisitionConfig(AcquisitionConfig): | ||
"""Acquisition config for Keysight QCS.""" | ||
|
||
kind: Literal["qcs-acquisition"] = "qcs-acquisition" | ||
|
||
state_iq_values: Annotated[Optional[NdArray], Field(repr=False)] = None |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
"""Utils for pulse handling.""" | ||
|
||
from collections import defaultdict | ||
from collections.abc import Iterable | ||
from typing import Union | ||
|
||
from keysight import qcs | ||
|
||
from qibolab._core.pulses import Drag, Envelope, Gaussian, PulseId, Rectangular | ||
from qibolab._core.pulses.pulse import PulseLike | ||
|
||
NS_TO_S = 1e-9 | ||
|
||
|
||
def generate_qcs_envelope(shape: Envelope) -> qcs.Envelope: | ||
"""Converts a Qibolab pulse envelope to a QCS Envelope object.""" | ||
if isinstance(shape, Rectangular): | ||
return qcs.ConstantEnvelope() | ||
|
||
elif isinstance(shape, (Gaussian, Drag)): | ||
return qcs.GaussianEnvelope(shape.rel_sigma) | ||
|
||
else: | ||
# TODO: Rework this code to support other Qibolab pulse envelopes | ||
# raw_envelope = shape.i(num_samples) + 1j * shape.q(num_samples) | ||
# return qcs.ArbitraryEnvelope( | ||
# times=np.linspace(0, 1, num_samples), amplitudes=raw_envelope | ||
# ) | ||
raise Exception("Envelope not supported") | ||
|
||
|
||
def process_acquisition_channel_pulses( | ||
program: qcs.Program, | ||
pulses: Iterable[PulseLike], | ||
frequency: Union[float, qcs.Scalar], | ||
virtual_channel: qcs.Channels, | ||
probe_virtual_channel: qcs.Channels, | ||
sweeper_pulse_map: defaultdict[PulseId, dict[str, qcs.Scalar]], | ||
classifier: qcs.Classifier = None, | ||
): | ||
"""Processes Qibolab pulses on the acquisition channel into QCS hardware | ||
instructions and adds it to the current program. | ||
|
||
Arguments: | ||
program (qcs.Program): Program object for the current sequence. | ||
pulses (Iterable[PulseLike]): Array of pulse objects to be processed. | ||
frequency (Union[float, qcs.Scalar]): Frequency of the channel. | ||
virtual_channel (qcs.Channels): QCS virtual digitizer channel. | ||
probe_virtual_channel (qcs.Channels): QCS virtual AWG channel connected to the digitzer. | ||
sweeper_pulse_map (defaultdict[PulseId, dict[str, qcs.Scalar]]): Map of pulse ID to map of parameter | ||
to be swept and corresponding QCS variable. | ||
""" | ||
|
||
for pulse in pulses: | ||
sweep_param_map = sweeper_pulse_map.get(pulse.id, {}) | ||
|
||
if pulse.kind == "delay": | ||
qcs_pulse = qcs.Delay( | ||
sweep_param_map.get("duration", pulse.duration * NS_TO_S) | ||
) | ||
program.add_waveform(qcs_pulse, virtual_channel) | ||
program.add_waveform(qcs_pulse, probe_virtual_channel) | ||
|
||
elif pulse.kind == "acquisition": | ||
duration = sweep_param_map.get("duration", pulse.duration * NS_TO_S) | ||
program.add_acquisition(duration, virtual_channel) | ||
|
||
elif pulse.kind == "readout": | ||
sweep_param_map = sweeper_pulse_map.get(pulse.probe.id, {}) | ||
qcs_pulse = qcs.RFWaveform( | ||
duration=sweep_param_map.get( | ||
"duration", pulse.probe.duration * NS_TO_S | ||
), | ||
envelope=generate_qcs_envelope(pulse.probe.envelope), | ||
amplitude=sweep_param_map.get("amplitude", pulse.probe.amplitude), | ||
rf_frequency=frequency, | ||
instantaneous_phase=sweep_param_map.get( | ||
"relative_phase", pulse.probe.relative_phase | ||
), | ||
) | ||
integration_filter = qcs.IntegrationFilter(qcs_pulse) | ||
program.add_waveform(qcs_pulse, probe_virtual_channel) | ||
program.add_acquisition(integration_filter, virtual_channel, classifier) | ||
|
||
|
||
def process_iq_channel_pulses( | ||
program: qcs.Program, | ||
pulses: Iterable[PulseLike], | ||
frequency: Union[float, qcs.Scalar], | ||
virtual_channel: qcs.Channels, | ||
sweeper_pulse_map: defaultdict[PulseId, dict[str, qcs.Scalar]], | ||
): | ||
"""Processes Qibolab pulses on the IQ channel into QCS hardware | ||
instructions and adds it to the current program. | ||
|
||
Arguments: | ||
program (qcs.Program): Program object for the current sequence. | ||
pulses (Iterable[PulseLike]): Array of pulse objects to be processed. | ||
frequency (Union[float, qcs.Scalar]): Frequency of the channel. | ||
virtual_channel (qcs.Channels): QCS virtual RF AWG channel. | ||
sweeper_pulse_map (defaultdict[PulseId, dict[str, qcs.Scalar]]): Map of pulse ID to map of parameter | ||
to be swept and corresponding QCS variable. | ||
""" | ||
qcs_pulses = [] | ||
for pulse in pulses: | ||
sweep_param_map = sweeper_pulse_map.get(pulse.id, {}) | ||
|
||
if pulse.kind == "delay": | ||
qcs_pulse = qcs.Delay( | ||
sweep_param_map.get("duration", pulse.duration * NS_TO_S) | ||
) | ||
elif pulse.kind == "virtualz": | ||
qcs_pulse = qcs.PhaseIncrement( | ||
phase=sweep_param_map.get("relative_phase", pulse.phase) | ||
) | ||
elif pulse.kind == "pulse": | ||
qcs_pulse = qcs.RFWaveform( | ||
duration=sweep_param_map.get("duration", pulse.duration * NS_TO_S), | ||
envelope=generate_qcs_envelope(pulse.envelope), | ||
amplitude=sweep_param_map.get("amplitude", pulse.amplitude), | ||
rf_frequency=frequency, | ||
instantaneous_phase=sweep_param_map.get( | ||
"relative_phase", pulse.relative_phase | ||
), | ||
) | ||
if pulse.envelope.kind == "drag": | ||
qcs_pulse = qcs_pulse.drag(coeff=pulse.envelope.beta) | ||
else: | ||
raise ValueError("Unrecognized pulse type", pulse.kind) | ||
|
||
qcs_pulses.append(qcs_pulse) | ||
|
||
program.add_waveform(qcs_pulses, virtual_channel) | ||
|
||
|
||
def process_dc_channel_pulses( | ||
program: qcs.Program, | ||
pulses: Iterable[PulseLike], | ||
virtual_channel: qcs.Channels, | ||
sweeper_pulse_map: defaultdict[PulseId, dict[str, qcs.Scalar]], | ||
): | ||
"""Processes Qibolab pulses on the DC channel into QCS hardware | ||
instructions and adds it to the current program. | ||
|
||
Arguments: | ||
program (qcs.Program): Program object for the current sequence. | ||
pulses (Iterable[PulseLike]): Array of pulse objects to be processed. | ||
virtual_channel (qcs.Channels): QCS virtual baseband AWG channel. | ||
sweeper_pulse_map (defaultdict[PulseId, dict[str, qcs.Scalar]]): Map of pulse ID to map of parameter | ||
to be swept and corresponding QCS variable. | ||
""" | ||
qcs_pulses = [] | ||
for pulse in pulses: | ||
sweep_param_map = sweeper_pulse_map.get(pulse.id, {}) | ||
if pulse.kind == "delay": | ||
qcs_pulse = qcs.Delay( | ||
sweep_param_map.get("duration", pulse.duration * NS_TO_S) | ||
) | ||
elif pulse.kind == "pulse": | ||
qcs_pulse = qcs.DCWaveform( | ||
duration=sweep_param_map.get("duration", pulse.duration * NS_TO_S), | ||
envelope=generate_qcs_envelope(pulse.envelope), | ||
amplitude=sweep_param_map.get("amplitude", pulse.amplitude), | ||
) | ||
else: | ||
raise ValueError("Unrecognized pulse type", pulse.kind) | ||
qcs_pulses.append(qcs_pulse) | ||
|
||
program.add_waveform(qcs_pulses, virtual_channel) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
"""Qibolab driver for Keysight QCS instrument set.""" | ||
|
||
from collections import defaultdict | ||
from functools import reduce | ||
from typing import ClassVar | ||
|
||
import numpy as np | ||
from keysight import qcs | ||
|
||
from qibolab._core.components import AcquisitionChannel, Config, DcChannel, IqChannel | ||
from qibolab._core.execution_parameters import AveragingMode, ExecutionParameters | ||
from qibolab._core.identifier import ChannelId, Result | ||
from qibolab._core.instruments.abstract import Controller | ||
from qibolab._core.pulses import PulseId | ||
from qibolab._core.sequence import InputOps, PulseSequence | ||
from qibolab._core.sweeper import ParallelSweepers | ||
|
||
from .pulse import ( | ||
process_acquisition_channel_pulses, | ||
process_dc_channel_pulses, | ||
process_iq_channel_pulses, | ||
) | ||
from .results import fetch_result, parse_result | ||
from .sweep import process_sweepers | ||
|
||
NS_TO_S = 1e-9 | ||
|
||
__all__ = ["KeysightQCS"] | ||
|
||
|
||
class KeysightQCS(Controller): | ||
"""Driver for interacting with QCS controller server.""" | ||
|
||
bounds: str = "qcs/bounds" | ||
|
||
qcs_channel_map: qcs.ChannelMapper | ||
"""Map of QCS virtual channels to QCS physical channels.""" | ||
virtual_channel_map: dict[ChannelId, qcs.Channels] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking at the example platform here(*): https://github.com/qiboteam/qibolab_platforms_nqch/blob/qibolab-0.2/iqm5q/platform.py, I have the impression that this dictionary can be eliminated by using qibolab's In any case, I have no experience with these particular instruments, so if you find it easier to use it this way, it is fine with me. (*) Unrelated to the above, it would be useful to also have an example (not necessarily real/working) platform here, but we can postpone this for later, when we have better testing or even better documentation for how to use each driver. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer to have a direct mapping because other approaches would involve some type casting or eval to get the necessary parameters from the The |
||
"""Map of Qibolab channel IDs to QCS virtual channels.""" | ||
sampling_rate: ClassVar[float] = ( | ||
qcs.SAMPLE_RATES[qcs.InstrumentEnum.M5300AWG] * NS_TO_S | ||
) | ||
|
||
def connect(self): | ||
self.backend = qcs.HclBackend(self.qcs_channel_map, hw_demod=True) | ||
self.backend.is_system_ready() | ||
|
||
def create_program( | ||
self, | ||
sequence: PulseSequence, | ||
configs: dict[str, Config], | ||
sweepers: list[ParallelSweepers], | ||
num_shots: int, | ||
) -> tuple[qcs.Program, list[tuple[int, int]]]: | ||
|
||
# SWEEPER MANAGEMENT | ||
probe_channel_ids = { | ||
chan.probe | ||
for chan in self.channels.values() | ||
if isinstance(chan, AcquisitionChannel) | ||
} | ||
|
||
( | ||
hardware_sweepers, | ||
software_sweepers, | ||
sweeper_channel_map, | ||
sweeper_pulse_map, | ||
) = process_sweepers(sweepers, probe_channel_ids) | ||
# Here we are telling the program to run hardware sweepers first, then software sweepers | ||
# It is essential that we match the original sweeper order to the modified sweeper order | ||
# to reconcile the results at the end | ||
sweep = lambda program, sweepers: program.sweep(*sweepers) | ||
program = reduce( | ||
sweep, | ||
software_sweepers, | ||
reduce(sweep, hardware_sweepers, qcs.Program()).n_shots(num_shots), | ||
) | ||
|
||
# WAVEFORM COMPILATION | ||
# Iterate over channels and convert qubit pulses to QCS waveforms | ||
for channel_id in sequence.channels: | ||
channel = self.channels[channel_id] | ||
virtual_channel = self.virtual_channel_map[channel_id] | ||
|
||
if isinstance(channel, AcquisitionChannel): | ||
probe_channel_id = channel.probe | ||
classifier_reference = configs[channel_id].state_iq_values | ||
process_acquisition_channel_pulses( | ||
program=program, | ||
pulses=sequence.channel(channel_id), | ||
frequency=sweeper_channel_map.get( | ||
probe_channel_id, configs[probe_channel_id].frequency | ||
), | ||
virtual_channel=virtual_channel, | ||
probe_virtual_channel=self.virtual_channel_map[probe_channel_id], | ||
sweeper_pulse_map=sweeper_pulse_map, | ||
classifier=( | ||
None | ||
if classifier_reference is None | ||
else qcs.MinimumDistanceClassifier(classifier_reference) | ||
), | ||
) | ||
|
||
elif isinstance(channel, IqChannel): | ||
process_iq_channel_pulses( | ||
program=program, | ||
pulses=sequence.channel(channel_id), | ||
frequency=sweeper_channel_map.get( | ||
channel_id, configs[channel_id].frequency | ||
), | ||
virtual_channel=virtual_channel, | ||
sweeper_pulse_map=sweeper_pulse_map, | ||
) | ||
|
||
elif isinstance(channel, DcChannel): | ||
process_dc_channel_pulses( | ||
program=program, | ||
pulses=sequence.channel(channel_id), | ||
virtual_channel=virtual_channel, | ||
sweeper_pulse_map=sweeper_pulse_map, | ||
) | ||
|
||
return program | ||
|
||
def play( | ||
self, | ||
configs: dict[str, Config], | ||
sequences: list[PulseSequence], | ||
options: ExecutionParameters, | ||
sweepers: list[ParallelSweepers], | ||
) -> dict[int, Result]: | ||
|
||
if options.relaxation_time is not None: | ||
self.backend._init_time = int(options.relaxation_time) | ||
|
||
ret: dict[PulseId, np.ndarray] = {} | ||
for sequence in sequences: | ||
results = self.backend.apply( | ||
self.create_program( | ||
sequence.align_to_delays(), configs, sweepers, options.nshots | ||
) | ||
).results | ||
acquisition_map: defaultdict[qcs.Channels, list[InputOps]] = defaultdict( | ||
list | ||
) | ||
|
||
for channel_id, input_op in sequence.acquisitions: | ||
channel = self.virtual_channel_map[channel_id] | ||
acquisition_map[channel].append(input_op) | ||
|
||
averaging = options.averaging_mode is not AveragingMode.SINGLESHOT | ||
for channel, input_ops in acquisition_map.items(): | ||
raw = fetch_result( | ||
results=results, | ||
channel=channel, | ||
acquisition_type=options.acquisition_type, | ||
averaging=averaging, | ||
) | ||
|
||
for result, input_op in zip(raw.values(), input_ops): | ||
|
||
ret[input_op.id] = parse_result(result, options) | ||
|
||
return ret | ||
|
||
def disconnect(self): | ||
pass |
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.
What is the motivation to include
Drag
here? It seems that this always returns a Gaussian, completely ignoring the DRAGbeta
parameter. If DRAG is not available then doesn't it make more sense to just raise an error, like for all other shapes?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.
So the DRAG is implemented here at https://github.com/qiboteam/qibolab/pull/944/files#diff-e3c18ab9440f0e68561a15f5e9497097a405ef090fd2612f4f3a362c71c83502R126-R127
and I need the gaussian as the base envelope for the summation of the base and derivative envelope.
(https://quantumbenchmark.com/docs/qcs/api/channels.html#rj-drag)
I could implement the DRAG based on Qibolab's implementation, but I would not like to create a static envelope especially when duration sweepers can change the envelope size.
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.
@sorewachigauyo I understand your concern, and now that you explained I'm even fine with your solution.
Not to enforce the change request, but as a general note, we suspect that most of the APIs are actually implementing duration sweepers in the same inefficient way you would do using Qibolab's envelopes (i.e. evaluating on software all the required arrays and uploading them).
This is not happening for QM, but there is a dedicated feature linked to that, and we're now exposing it as a separate sweeper (where the envelope is not uploaded multiple times, but interpolated on the device).
qibolab/src/qibolab/_core/sweeper.py
Line 25 in f74c334
If you suspect Keysight to be able to interpolate as well, I'd recommend you to verify it (as much as you can), and expose it through that sweeper, instead of the bare duration one (there will be a fallback at some level, but they may have different caveats, related to timing and approximation).