Source code for qibolab.native

from collections import defaultdict
from dataclasses import dataclass, field, fields, replace
from typing import List, Optional, Union

from qibolab.pulses import (
    CouplerFluxPulse,
    FluxPulse,
    PulseConstructor,
    PulseSequence,
    PulseType,
)


[docs]@dataclass class NativePulse: """Container with parameters required to generate a pulse implementing a native gate.""" name: str """Name of the gate that the pulse implements.""" duration: int amplitude: float shape: str pulse_type: PulseType qubit: "qubits.Qubit" frequency: int = 0 relative_start: int = 0 """Relative start is relevant for two-qubit gate operations which correspond to a pulse sequence.""" # used for qblox if_frequency: Optional[int] = None # TODO: Note sure if the following parameters are useful to be in the runcard start: int = 0 phase: float = 0.0
[docs] @classmethod def from_dict(cls, name, pulse, qubit): """Parse the dictionary provided by the runcard. Args: name (str): Name of the native gate (dictionary key). pulse (dict): Dictionary containing the parameters of the pulse implementing the gate, as loaded from the runcard. qubits (:class:`qibolab.platforms.abstract.Qubit`): Qubit that the pulse is acting on """ kwargs = pulse.copy() kwargs["pulse_type"] = PulseType(kwargs.pop("type")) kwargs["qubit"] = qubit return cls(name, **kwargs)
@property def raw(self): data = { fld.name: getattr(self, fld.name) for fld in fields(self) if getattr(self, fld.name) is not None } del data["name"] del data["start"] if self.pulse_type is PulseType.FLUX: del data["frequency"] del data["phase"] data["qubit"] = self.qubit.name data["type"] = data.pop("pulse_type").value return data
[docs] def pulse(self, start, relative_phase=0.0): """Construct the :class:`qibolab.pulses.Pulse` object implementing the gate. Args: start (int): Start time of the pulse in the sequence. relative_phase (float): Relative phase of the pulse. Returns: A :class:`qibolab.pulses.DrivePulse` or :class:`qibolab.pulses.DrivePulse` or :class:`qibolab.pulses.FluxPulse` with the pulse parameters of the gate. """ if self.pulse_type is PulseType.FLUX: return FluxPulse( start + self.relative_start, self.duration, self.amplitude, self.shape, channel=self.qubit.flux.name, qubit=self.qubit.name, ) pulse_cls = PulseConstructor[self.pulse_type.name].value channel = getattr(self.qubit, self.pulse_type.name.lower()).name return pulse_cls( start + self.relative_start, self.duration, self.amplitude, self.frequency, relative_phase, self.shape, channel, qubit=self.qubit.name, )
[docs]@dataclass class VirtualZPulse: """Container with parameters required to add a virtual Z phase in a pulse sequence.""" phase: float qubit: "qubits.Qubit" @property def raw(self): return {"type": "virtual_z", "phase": self.phase, "qubit": self.qubit.name}
[docs]@dataclass class CouplerPulse: """Container with parameters required to add a coupler pulse in a pulse sequence.""" duration: int amplitude: float shape: str coupler: "couplers.Coupler" relative_start: int = 0
[docs] @classmethod def from_dict(cls, pulse, coupler): """Parse the dictionary provided by the runcard. Args: name (str): Name of the native gate (dictionary key). pulse (dict): Dictionary containing the parameters of the pulse implementing the gate, as loaded from the runcard. coupler (:class:`qibolab.platforms.abstract.Coupler`): Coupler that the pulse is acting on """ kwargs = pulse.copy() kwargs["coupler"] = coupler kwargs.pop("type") return cls(**kwargs)
@property def raw(self): return { "type": "coupler", "duration": self.duration, "amplitude": self.amplitude, "shape": self.shape, "coupler": self.coupler.name, "relative_start": self.relative_start, }
[docs] def pulse(self, start): """Construct the :class:`qibolab.pulses.Pulse` object implementing the gate. Args: start (int): Start time of the pulse in the sequence. Returns: A :class:`qibolab.pulses.FluxPulse` with the pulse parameters of the gate. """ return CouplerFluxPulse( start + self.relative_start, self.duration, self.amplitude, self.shape, channel=self.coupler.flux.name, qubit=self.coupler.name, )
[docs]@dataclass class NativeSequence: """List of :class:`qibolab.platforms.native.NativePulse` objects implementing a gate. Relevant for two-qubit gates, which usually require a sequence of pulses to be implemented. These pulses may act on qubits different than the qubits the gate is targeting. """ name: str pulses: List[Union[NativePulse, VirtualZPulse]] = field(default_factory=list) coupler_pulses: List[CouplerPulse] = field(default_factory=list)
[docs] @classmethod def from_dict(cls, name, sequence, qubits, couplers): """Constructs the native sequence from the dictionaries provided in the runcard. Args: name (str): Name of the gate the sequence is applying. sequence (dict): Dictionary describing the sequence as provided in the runcard. qubits (list): List of :class:`qibolab.qubits.Qubit` object for all qubits in the platform. All qubits are required because the sequence may be acting on qubits that the implemented gate is not targeting. couplers (list): List of :class:`qibolab.couplers.Coupler` object for all couplers in the platform. All couplers are required because the sequence may be acting on couplers that the implemented gate is not targeting. """ pulses = [] coupler_pulses = [] # If sequence contains only one pulse dictionary, convert it into a list that can be iterated below if isinstance(sequence, dict): sequence = [sequence] for i, pulse in enumerate(sequence): pulse = pulse.copy() pulse_type = pulse.pop("type") if pulse_type == "coupler": pulse["coupler"] = couplers[pulse.pop("coupler")] coupler_pulses.append(CouplerPulse(**pulse)) else: qubit = qubits[pulse.pop("qubit")] if pulse_type == "virtual_z": phase = pulse["phase"] pulses.append(VirtualZPulse(phase, qubit)) else: pulses.append( NativePulse( f"{name}{i}", **pulse, pulse_type=PulseType(pulse_type), qubit=qubit, ) ) return cls(name, pulses, coupler_pulses)
@property def raw(self): pulses = [pulse.raw for pulse in self.pulses] coupler_pulses = [pulse.raw for pulse in self.coupler_pulses] return pulses + coupler_pulses
[docs] def sequence(self, start=0): """Creates a :class:`qibolab.pulses.PulseSequence` object implementing the sequence.""" sequence = PulseSequence() virtual_z_phases = defaultdict(int) for pulse in self.pulses: if isinstance(pulse, NativePulse): sequence.add(pulse.pulse(start=start)) else: virtual_z_phases[pulse.qubit.name] += pulse.phase for coupler_pulse in self.coupler_pulses: sequence.add(coupler_pulse.pulse(start=start)) # TODO: Maybe ``virtual_z_phases`` should be an attribute of ``PulseSequence`` return sequence, virtual_z_phases
[docs]@dataclass class SingleQubitNatives: """Container with the native single-qubit gates acting on a specific qubit.""" RX: Optional[NativePulse] = None """Pulse to drive the qubit from state 0 to state 1.""" RX12: Optional[NativePulse] = None """Pulse to drive to qubit from state 1 to state 2.""" MZ: Optional[NativePulse] = None """Measurement pulse.""" @property def RX90(self) -> NativePulse: """RX90 native pulse is inferred from RX by halving its amplitude.""" return replace(self.RX, name="RX90", amplitude=self.RX.amplitude / 2.0)
[docs] @classmethod def from_dict(cls, qubit, native_gates): """Parse native gates of the qubit from the runcard. Args: qubit (:class:`qibolab.qubits.Qubit`): Qubit object that the native gates are acting on. native_gates (dict): Dictionary with native gate pulse parameters as loaded from the runcard. """ pulses = { n: NativePulse.from_dict(n, pulse, qubit=qubit) for n, pulse in native_gates.items() } return cls(**pulses)
@property def raw(self): """Serialize native gate pulses. ``None`` gates are not included. """ data = {} for fld in fields(self): attr = getattr(self, fld.name) if attr is not None: data[fld.name] = attr.raw del data[fld.name]["qubit"] return data
[docs]@dataclass class CouplerNatives: """Container with the native single-qubit gates acting on a specific qubit.""" CP: Optional[NativePulse] = None """Pulse to activate the coupler."""
[docs] @classmethod def from_dict(cls, coupler, native_gates): """Parse coupler native gates from the runcard. Args: coupler (:class:`qibolab.couplers.Coupler`): Coupler object that the native pulses are acting on. native_gates (dict): Dictionary with native gate pulse parameters as loaded from the runcard [Reusing the dict from qubits]. """ pulses = { n: CouplerPulse.from_dict(pulse, coupler=coupler) for n, pulse in native_gates.items() } return cls(**pulses)
@property def raw(self): """Serialize native gate pulses. ``None`` gates are not included. """ data = {} for fld in fields(self): attr = getattr(self, fld.name) if attr is not None: data[fld.name] = attr.raw return data
[docs]@dataclass class TwoQubitNatives: """Container with the native two-qubit gates acting on a specific pair of qubits.""" CZ: Optional[NativeSequence] = field(default=None, metadata={"symmetric": True}) CNOT: Optional[NativeSequence] = field(default=None, metadata={"symmetric": False}) iSWAP: Optional[NativeSequence] = field(default=None, metadata={"symmetric": True}) @property def symmetric(self): """Check if the defined two-qubit gates are symmetric between target and control qubits.""" return all( fld.metadata["symmetric"] or getattr(self, fld.name) is None for fld in fields(self) )
[docs] @classmethod def from_dict(cls, qubits, couplers, native_gates): sequences = { n: NativeSequence.from_dict(n, seq, qubits, couplers) for n, seq in native_gates.items() } return cls(**sequences)
@property def raw(self): data = {} for fld in fields(self): gate = getattr(self, fld.name) if gate is not None: data[fld.name] = gate.raw return data