Source code for qanary.parameters

from typing import Sequence

import numpy as np
from qcodes import validators as vals
from qcodes.parameters import Parameter

# Public API
__all__ = [
    "ParameterMixin",
    "VirtualGate",
    "MultiChannelParameter",
]


def _root_instrument(param: Parameter):
    instr = param.instrument
    while hasattr(instr, "parent") and instr.parent is not None:
        instr = instr.parent
    return instr


[docs] class ParameterMixin: """ Alias an existing QCoDeS parameter for station metadata. The returned object keeps the original parameter class and state while optionally replacing its short name and label. Args: param (Parameter): Existing QCoDeS parameter. name (str): Optional short name override. label (str): Optional display-label override. param_type (str): Category stored in the parameter snapshot. Defaults to ``"gate"``. """ def __init__( self, param: Parameter, name: str = None, label: str = None, param_type: str = "gate", ): self.__dict__.update(param.__dict__) self.__class__ = param.__class__ self.param_type = param_type if name: self._short_name = name if label: self._label = label
[docs] class VirtualGate(Parameter): """ Linear virtual coordinate composed from parameters on one instrument. """ def __init__( self, gates: list[Parameter], factors: list[float] = None, offsets: list[float] = None, rot_angle_deg: float | None = None, points: list[tuple[float, float]] = None, name: str | None = None, label: str | None = None, param_type: str = "virtual_gate_linear", **kwargs, ): """ Create a virtual gate from coefficients, a rotation, or two points. Setting virtual value ``v`` writes ``factor * v + offset`` to each underlying gate. Reading returns a tuple containing every physical gate value. Args: gates (list[Parameter]): Underlying gate parameters. They must share one root instrument. factors (list[float]): Scale factor for each gate. offsets (list[float]): Offset for each gate. rot_angle_deg (float | None): For exactly two gates, replace *factors* with the unit vector at this counter-clockwise angle and set both offsets to zero. points (list[tuple[float, float]]): For exactly two gates, use the first point as the offset and the normalized direction to the second point as the factors. This takes precedence when supplied together with a rotation. name (str | None): QCoDeS parameter name. Generated from gate names when omitted. label (str | None): Display label. Generated from gate names when omitted. param_type (str): Snapshot category. Defaults to ``"virtual_gate_linear"``. **kwargs: Additional keyword arguments passed to QCoDeS :class:`~qcodes.parameters.Parameter`. Raises: ValueError: If coefficient lengths differ from the gate count, a two-dimensional definition does not receive exactly two gates, the two points coincide, or gates have different roots. """ self.gates = gates self.param_type = param_type self.factors = factors self.offsets = offsets self.points = points self.rot_angle_deg = rot_angle_deg if self.factors is not None and self.offsets is not None: if len(gates) != len(self.factors) or len(gates) != len(self.offsets): raise ValueError( "The number of gates, factors, and offsets must be the same" ) if rot_angle_deg is not None: if len(self.gates) != 2: raise ValueError("Rotation is only supported for exactly two gates") theta = np.deg2rad(rot_angle_deg) self.factors = [np.cos(theta), np.sin(theta)] self.offsets = [0, 0] if self.points is not None: if len(self.gates) != 2: raise ValueError( "Point-based virtual gates are only supported for exactly two gates" ) P1 = np.asarray(self.points[0], dtype=float) P2 = np.asarray(self.points[1], dtype=float) direction = P2 - P1 distance = np.linalg.norm(direction) if distance == 0: raise ValueError("The two points must be different") direction /= distance self.factors = direction.tolist() self.offsets = P1.tolist() root = _root_instrument(gates[0]) if not all(_root_instrument(ch) is root for ch in gates): raise ValueError("All gates must belong to the same root instrument") if name is None: gate_names = "_".join(g.name for g in gates) name = f"virtual_gate_{gate_names}" if label is None: gate_names = " ".join(g.name for g in gates) label = f"Virtual Gate {gate_names}" unit = gates[0].unit super().__init__( name=name, label=label, unit=unit, instrument=root, vals=vals.Numbers(), **kwargs, )
[docs] def set_raw(self, value: float): """Apply the configured linear transformation to every gate.""" for ch, f, o in zip(self.gates, self.factors, self.offsets): ch(f * value + o)
[docs] def get_raw(self) -> float: """Return the current values of all underlying gates as a tuple.""" vals = tuple(ch.get() for ch in self.gates) return vals
[docs] def snapshot_base(self, update=False, params_to_skip_update=None): """Extend the QCoDeS snapshot with the virtual-gate transformation.""" snap = super().snapshot_base( update=update, params_to_skip_update=params_to_skip_update, ) snap["param_type"] = self.param_type snap["gates"] = {ch.name: ch.full_name for ch in self.gates} snap["transformation"] = { "factors": [float(f) for f in self.factors], "offsets": [float(o) for o in self.offsets], } parts = [ f"{g.label} = {o:.3f} + {f:.3f} * V_virtual" for g, f, o in zip(self.gates, self.factors, self.offsets) ] snap["description"] = "Virtual Gate: [" + ", ".join(parts) + "]" snap["rot_angle_deg"] = self.rot_angle_deg snap["points"] = self.points return snap
[docs] class MultiChannelParameter(Parameter): """ Expose several channels on one instrument as a single parameter. """ def __init__( self, param: Sequence[Parameter], name: str = None, label: str = None, param_type: str = "gates", **kwargs, ): """ Create a parameter that writes the same value to every channel. Args: param (Sequence[Parameter]): One or more channels sharing a root instrument. name (str): Optional parameter name generated from channel names when omitted. label (str): Optional label generated from channel labels when omitted. param_type (str): Snapshot category. Defaults to ``"gates"``. **kwargs: Additional keyword arguments passed to QCoDeS :class:`~qcodes.parameters.Parameter`. Raises: ValueError: If no channels are supplied or their root instruments differ. """ channels = list(param) if not channels: raise ValueError("At least one channel must be provided") root = _root_instrument(channels[0]) if not all(_root_instrument(ch) is root for ch in channels): raise ValueError("All channels must belong to the same root instrument") if name is None: channel_names = "_".join(ch.name for ch in channels) name = f"multi_channel_parameter_{channel_names}" if label is None: label = "MultiChannelParameter: " + ", ".join(ch.label for ch in channels) unit = channels[0].unit super().__init__( name=name, label=label, unit=unit, instrument=root, vals=vals.Numbers(), **kwargs, ) self.channels = channels self.param_type = param_type
[docs] def set_raw(self, value: float): """ Write *value* to every channel. """ for ch in self.channels: ch(value)
[docs] def get_raw(self): """ Return the common channel value, or ``None`` if values differ. """ values = tuple(ch.get() for ch in self.channels) if all(v == values[0] for v in values): return float(values[0]) return None
[docs] def snapshot_base( self, update=False, params_to_skip_update=None, ): """ Extend the QCoDeS snapshot with channel identities and type. """ snap = super().snapshot_base( update=update, params_to_skip_update=params_to_skip_update, ) snap["param_type"] = self.param_type snap["channels"] = [ { "name": ch.full_name, "label": ch.label, "unit": ch.unit, } for ch in self.channels ] return snap