import math
from dataclasses import dataclass, field
import numpy as np
from qibo.config import raise_error
from qibolab.pulses import PulseType, Rectangular
from .ports import OPXIQ, OctaveInput, OctaveOutput
SAMPLING_RATE = 1
"""Sampling rate of Quantum Machines OPX in GSps."""
DEFAULT_INPUTS = {1: {}, 2: {}}
"""Default controller config section.
Inputs are always registered to avoid issues with automatic mixer
calibration when using Octaves.
"""
[docs]@dataclass
class QMConfig:
"""Configuration for communicating with the ``QuantumMachinesManager``."""
version: int = 1
controllers: dict = field(default_factory=dict)
octaves: dict = field(default_factory=dict)
elements: dict = field(default_factory=dict)
pulses: dict = field(default_factory=dict)
waveforms: dict = field(default_factory=dict)
digital_waveforms: dict = field(
default_factory=lambda: {"ON": {"samples": [(1, 0)]}}
)
integration_weights: dict = field(default_factory=dict)
mixers: dict = field(default_factory=dict)
[docs] def register_port(self, port):
"""Register controllers and octaves sections in the ``config``.
Args:
ports (QMPort): Port we are registering.
Contains information about the controller and port number and
some parameters, such as offset, gain, filter, etc.).
"""
if isinstance(port, OPXIQ):
self.register_port(port.i)
self.register_port(port.q)
else:
is_octave = isinstance(port, (OctaveOutput, OctaveInput))
controllers = self.octaves if is_octave else self.controllers
if port.device not in controllers:
if is_octave:
controllers[port.device] = {}
else:
controllers[port.device] = {
"analog_inputs": DEFAULT_INPUTS,
"digital_outputs": {},
}
device = controllers[port.device]
if port.key in device:
device[port.key].update(port.config)
else:
device[port.key] = port.config
if is_octave:
con = port.opx_port.i.device
number = port.opx_port.i.number
device["connectivity"] = con
self.register_port(port.opx_port)
self.controllers[con]["digital_outputs"][number] = {}
[docs] @staticmethod
def iq_imbalance(g, phi):
"""Creates the correction matrix for the mixer imbalance caused by the
gain and phase imbalances.
More information here:
https://docs.qualang.io/libs/examples/mixer-calibration/#non-ideal-mixer
Args:
g (float): relative gain imbalance between the I & Q ports (unit-less).
Set to 0 for no gain imbalance.
phi (float): relative phase imbalance between the I & Q ports (radians).
Set to 0 for no phase imbalance.
"""
c = np.cos(phi)
s = np.sin(phi)
N = 1 / ((1 - g**2) * (2 * c**2 - 1))
return [
float(N * x) for x in [(1 - g) * c, (1 + g) * s, (1 - g) * s, (1 + g) * c]
]
[docs] def register_drive_element(self, qubit, intermediate_frequency=0):
"""Register qubit drive elements and controllers in the QM config.
Args:
qubit (:class:`qibolab.platforms.utils.Qubit`): Qubit to add elements for.
intermediate_frequency (int): Intermediate frequency that the OPX
will send to this qubit. This frequency will be mixed with the
LO connected to the same channel.
"""
if f"drive{qubit.name}" not in self.elements:
if isinstance(qubit.drive.port, OPXIQ):
lo_frequency = math.floor(qubit.drive.lo_frequency)
self.elements[f"drive{qubit.name}"] = {
"mixInputs": {
"I": qubit.drive.port.i.pair,
"Q": qubit.drive.port.q.pair,
"lo_frequency": lo_frequency,
"mixer": f"mixer_drive{qubit.name}",
},
}
drive_g = qubit.mixer_drive_g
drive_phi = qubit.mixer_drive_phi
self.mixers[f"mixer_drive{qubit.name}"] = [
{
"intermediate_frequency": intermediate_frequency,
"lo_frequency": lo_frequency,
"correction": self.iq_imbalance(drive_g, drive_phi),
}
]
else:
self.elements[f"drive{qubit.name}"] = {
"RF_inputs": {"port": qubit.drive.port.pair},
"digitalInputs": qubit.drive.port.digital_inputs,
}
self.elements[f"drive{qubit.name}"].update(
{
"intermediate_frequency": intermediate_frequency,
"operations": {},
}
)
else:
self.elements[f"drive{qubit.name}"][
"intermediate_frequency"
] = intermediate_frequency
if isinstance(qubit.drive.port, OPXIQ):
self.mixers[f"mixer_drive{qubit.name}"][0][
"intermediate_frequency"
] = intermediate_frequency
[docs] def register_readout_element(
self, qubit, intermediate_frequency=0, time_of_flight=0, smearing=0
):
"""Register resonator elements and controllers in the QM config.
Args:
qubit (:class:`qibolab.platforms.utils.Qubit`): Qubit to add elements for.
intermediate_frequency (int): Intermediate frequency that the OPX
will send to this qubit. This frequency will be mixed with the
LO connected to the same channel.
"""
if f"readout{qubit.name}" not in self.elements:
if isinstance(qubit.readout.port, OPXIQ):
lo_frequency = math.floor(qubit.readout.lo_frequency)
self.elements[f"readout{qubit.name}"] = {
"mixInputs": {
"I": qubit.readout.port.i.pair,
"Q": qubit.readout.port.q.pair,
"lo_frequency": lo_frequency,
"mixer": f"mixer_readout{qubit.name}",
},
"outputs": {
"out1": qubit.feedback.port.i.pair,
"out2": qubit.feedback.port.q.pair,
},
}
readout_g = qubit.mixer_readout_g
readout_phi = qubit.mixer_readout_phi
self.mixers[f"mixer_readout{qubit.name}"] = [
{
"intermediate_frequency": intermediate_frequency,
"lo_frequency": lo_frequency,
"correction": self.iq_imbalance(readout_g, readout_phi),
}
]
else:
self.elements[f"readout{qubit.name}"] = {
"RF_inputs": {"port": qubit.readout.port.pair},
"RF_outputs": {"port": qubit.feedback.port.pair},
"digitalInputs": qubit.readout.port.digital_inputs,
}
self.elements[f"readout{qubit.name}"].update(
{
"intermediate_frequency": intermediate_frequency,
"operations": {},
"time_of_flight": time_of_flight,
"smearing": smearing,
}
)
else:
self.elements[f"readout{qubit.name}"][
"intermediate_frequency"
] = intermediate_frequency
if isinstance(qubit.readout.port, OPXIQ):
self.mixers[f"mixer_readout{qubit.name}"][0][
"intermediate_frequency"
] = intermediate_frequency
[docs] def register_flux_element(self, qubit, intermediate_frequency=0):
"""Register qubit flux elements and controllers in the QM config.
Args:
qubit (:class:`qibolab.platforms.utils.Qubit`): Qubit to add elements for.
intermediate_frequency (int): Intermediate frequency that the OPX
will send to this qubit. This frequency will be mixed with the
LO connected to the same channel.
"""
if f"flux{qubit.name}" not in self.elements:
self.elements[f"flux{qubit.name}"] = {
"singleInput": {
"port": qubit.flux.port.pair,
},
"intermediate_frequency": intermediate_frequency,
"operations": {},
}
else:
self.elements[f"flux{qubit.name}"][
"intermediate_frequency"
] = intermediate_frequency
[docs] def register_element(self, qubit, pulse, time_of_flight=0, smearing=0):
if pulse.type is PulseType.DRIVE:
# register drive element
if_frequency = pulse.frequency - math.floor(qubit.drive.lo_frequency)
self.register_drive_element(qubit, if_frequency)
# register flux element (if available)
if qubit.flux:
self.register_flux_element(qubit)
elif pulse.type is PulseType.READOUT:
# register readout element (if it does not already exist)
if_frequency = pulse.frequency - math.floor(qubit.readout.lo_frequency)
self.register_readout_element(qubit, if_frequency, time_of_flight, smearing)
# register flux element (if available)
if qubit.flux:
self.register_flux_element(qubit)
else:
# register flux element
self.register_flux_element(qubit, pulse.frequency)
[docs] def register_pulse(self, qubit, qmpulse):
"""Registers pulse, waveforms and integration weights in QM config.
Args:
qubit (:class:`qibolab.platforms.utils.Qubit`): Qubit that the pulse acts on.
pulse (:class:`qibolab.pulses.Pulse`): Pulse object to register.
Returns:
element (str): Name of the element this pulse will be played on.
Elements are a part of the QM config and are generated during
instantiation of the Qubit objects. They are named as
"drive0", "drive1", "flux0", "readout0", ...
"""
pulse = qmpulse.pulse
if qmpulse.operation not in self.pulses:
if pulse.type is PulseType.DRIVE:
serial_i = self.register_waveform(pulse, "i")
serial_q = self.register_waveform(pulse, "q")
self.pulses[qmpulse.operation] = {
"operation": "control",
"length": pulse.duration,
"waveforms": {"I": serial_i, "Q": serial_q},
"digital_marker": "ON",
}
# register drive pulse in elements
self.elements[f"drive{qubit.name}"]["operations"][
qmpulse.operation
] = qmpulse.operation
elif pulse.type is PulseType.FLUX:
serial = self.register_waveform(pulse)
self.pulses[qmpulse.operation] = {
"operation": "control",
"length": pulse.duration,
"waveforms": {
"single": serial,
},
}
# register flux pulse in elements
self.elements[f"flux{qubit.name}"]["operations"][
qmpulse.operation
] = qmpulse.operation
elif pulse.type is PulseType.READOUT:
serial_i = self.register_waveform(pulse, "i")
serial_q = self.register_waveform(pulse, "q")
self.register_integration_weights(qubit, pulse.duration)
self.pulses[qmpulse.operation] = {
"operation": "measurement",
"length": pulse.duration,
"waveforms": {
"I": serial_i,
"Q": serial_q,
},
"integration_weights": {
"cos": f"cosine_weights{qubit.name}",
"sin": f"sine_weights{qubit.name}",
"minus_sin": f"minus_sine_weights{qubit.name}",
},
"digital_marker": "ON",
}
# register readout pulse in elements
self.elements[f"readout{qubit.name}"]["operations"][
qmpulse.operation
] = qmpulse.operation
else:
raise_error(TypeError, f"Unknown pulse type {pulse.type.name}.")
[docs] def register_integration_weights(self, qubit, readout_len):
"""Registers integration weights in QM config.
Args:
qubit (:class:`qibolab.platforms.quantum_machines.Qubit`): Qubit
object that the integration weights will be used for.
readout_len (int): Duration of the readout pulse in ns.
"""
angle = 0
cos, sin = np.cos(angle), np.sin(angle)
if qubit.kernel is None:
convert = lambda x: [(x, readout_len)]
else:
cos = qubit.kernel * cos
sin = qubit.kernel * sin
convert = lambda x: x
self.integration_weights.update(
{
f"cosine_weights{qubit.name}": {
"cosine": convert(cos),
"sine": convert(-sin),
},
f"sine_weights{qubit.name}": {
"cosine": convert(sin),
"sine": convert(cos),
},
f"minus_sine_weights{qubit.name}": {
"cosine": convert(-sin),
"sine": convert(-cos),
},
}
)