Source code for c3.signal.gates

import hjson
import numpy as np
import tensorflow as tf
from c3.c3objs import C3obj, Quantity, hjson_encode
from c3.signal.pulse import Envelope, Carrier
from c3.libraries.envelopes import gaussian_nonorm
import warnings
from typing import List, Dict, Any
import copy
from c3.libraries.constants import GATES
from c3.utils.qt_utils import np_kron_n, insert_mat_kron
from c3.utils.tf_utils import tf_project_to_comp
from c3.signal.pulse import components as comp_lib


def _from_dict_get_name_back_compat(cfg: dict, def_name: str) -> str:
    """
    Method to use in from_dict to get the name of the Instruction in a backwards compatible manner.

    Parameters
    ----------
    cfg: dict
        Configuration dictionary, including 'name' or '_name' key.
    def_name: str
        Name to give if no name is found in the configuration.

    Returns
    -------
    Name of the instruction
    """
    if "name" in cfg:
        return cfg["name"]
    if "_name" in cfg:
        return cfg["_name"]

    return def_name


[docs]class Instruction: """ Collection of components making up the control signal for a line. Parameters ---------- ideal: np.ndarray Ideal gate that the instruction should emulate. channels : list List of channel names (strings). t_start : np.float64 Start of the signal. t_end : np.float64 End of the signal. Attributes ---------- comps : dict Nested dictionary with lines and components as keys Example: comps = { 'channel_1' : { 'envelope1': envelope1, 'envelope2': envelope2, 'carrier': carrier } } """ def __init__( self, name: str = " ", targets: list = None, params: dict = None, ideal: np.ndarray = None, channels: List[str] = [], t_start: float = 0.0, t_end: float = 0.0, ): self.set_name(name) self.set_ideal(ideal) self.targets = targets self.params: dict = {} if isinstance(params, dict): self.params.update(params) if t_start is not None: warnings.warn( "t_start will be removed in the future. Do not set it anymore.", category=DeprecationWarning, ) self.t_start = t_start self.t_end = t_end self.comps: Dict[str, Dict[str, C3obj]] = dict() self._options: Dict[str, dict] = dict() self.fixed_t_end = True for chan in channels: self.comps[chan] = dict() self._options[chan] = dict() self._timings: Dict[str, tuple] = dict() @property def name(self) -> str: return self._name @name.setter def name(self, name: str): self.set_name(name)
[docs] def as_openqasm(self) -> dict: asdict: Dict[str, Any] = { "name": self._name, "qubits": self.targets, "params": self.params, } if self.ideal: asdict["ideal"] = self.ideal return asdict
[docs] def set_name(self, name, ideal=None): self._name = name self.set_ideal(None) # sets from name
[docs] def set_ideal(self, ideal): if ideal is not None: self.ideal = ideal else: gate_list = [] # legacy use for key in self._name.split(":"): if key in GATES: gate_list.append(GATES[key]) else: warnings.warn( f"No ideal gate found for gate: {key}. Use set_ideal() explicitly." ) self.ideal = np_kron_n(gate_list)
[docs] def get_ideal_gate(self, dims, index=None): if self.ideal is None: raise Exception( "C3:ERROR: No ideal representation definded for gate" f" {self.get_key()}" ) targets = self.targets if targets is None: targets = list(range(len(dims))) ideal_gate = insert_mat_kron( [2] * len(dims), # we compare to the computational basis targets, [self.ideal], ) if index: ideal_gate = tf_project_to_comp( ideal_gate, dims=[2] * len(dims), index=index ) return ideal_gate
[docs] def get_key(self) -> str: if self.targets is None: return self._name return self._name + str(self.targets)
[docs] def asdict(self) -> dict: components = {} # type:ignore for chan, item in self.comps.items(): components[chan] = {} for key, comp in item.items(): components[chan][key] = comp.asdict() out_dict = copy.deepcopy(self.__dict__) out_dict["name"] = out_dict["_name"] out_dict.pop("_name") out_dict["ideal"] = out_dict["ideal"] out_dict.pop("_timings") out_dict.pop("t_start") out_dict.pop("t_end") out_dict["gate_length"] = self.t_end - self.t_start out_dict["drive_channels"] = out_dict.pop("comps") return out_dict
[docs] def from_dict(self, cfg, name=None): self.__init__( name=_from_dict_get_name_back_compat(cfg, name), targets=cfg["targets"] if "targets" in cfg else None, params=cfg["params"] if "params" in cfg else None, ideal=np.array(cfg["ideal"]) if "ideal" in cfg else None, channels=cfg["drive_channels"].keys(), t_start=0.0, t_end=cfg["gate_length"], ) options = cfg.pop("_options", None) components = cfg.pop("drive_channels") self.__dict__.update(cfg) for drive_chan, comps in components.items(): for comp, props in comps.items(): ctype = props.pop("c3type") if "name" not in props: props["name"] = comp self.add_component( comp_lib[ctype](**props), chan=drive_chan, options=options[drive_chan][comp] if options else None, name=comp, )
[docs] def __repr__(self): return f"Instruction[{self.get_key()}]"
[docs] def __str__(self) -> str: return hjson.dumps(self.asdict(), default=hjson_encode)
[docs] def add_component(self, comp: C3obj, chan: str, options=None, name=None) -> None: """ Add one component, e.g. an envelope, local oscillator, to a channel. Parameters ---------- comp : C3obj Component to be added. chan : str Identifier for the target channel options: dict Options for this component, available keys are delay: Quantity Delay execution of this component by a certain time trigger_comp: Tuple[str] Tuple of (chan, name) of component acting as trigger. Delay time will be counted beginning with end of trigger t_final_cut: Quantity Length of component, signal will be cut after this time. Also used for the trigger. If not given this invokation from components `t_final` will be attempted. drag: bool Use drag correction for this component. t_end: float End of this component. None will use the full instruction. If t_end is None and t_start is given a length will be inherited from the instruction. """ if chan in self.comps and comp.name in self.comps[chan]: print( f"Component of instruction {self.get_key()} has been overwritten: Channel: {chan}, Component: {comp.name}", ) if name is None: name = comp.name self.comps[chan][name] = comp if options is None: options = dict() for k, v in options.items(): if isinstance(v, dict): options[k] = Quantity(**v) self._options[chan][name] = options
[docs] def get_optimizable_parameters(self): parameter_list = list() for chan in self.comps.keys(): for comp in self.comps[chan]: for par_name, par_value in self.comps[chan][comp].params.items(): parameter_list.append( ([self.get_key(), chan, comp, par_name], par_value) ) for option_name, option_val in self._options[chan][comp].items(): if isinstance(option_val, Quantity): parameter_list.append( ( [ self.get_key(), chan, comp, option_name, ], option_val, ) ) return parameter_list
[docs] def get_timings(self, chan, name, minimal_time=False): key = chan + "-" + name if key in self._timings: return self._timings[key] opts = self._options[chan][name] comp = self.comps[chan][name] t_start = self.t_start if "delay" in opts: t_start += opts["delay"].get_value() if "trigger_comp" in opts: t_start += self.get_timings(*opts["trigger_comp"])[1] if "t_final_cut" in opts: t_end = t_start + opts["t_final_cut"].get_value() elif isinstance(comp, Envelope): t_end = t_start + comp.params["t_final"].get_value() elif minimal_time: t_end = t_start else: t_end = self.t_end # TODO: The following needs to go. We need proper configuration or an error. # if t_end > self.t_end: # if self.fixed_t_end and not minimal_time: # warnings.warn( # f"Length of instruction {self.get_key()} is fixed, but cuts at least one component. {chan}-{name} is should end @ {t_end}, but instruction ends at {self.t_end}" # ) # t_end = self.t_end # elif minimal_time: # pass # else: # # TODO make compatible with generator # warnings.warn( # f"""T_end of {self.get_key()} has been extended to {t_end}. This will however only take effect on the next signal generation""" # ) # self.t_end = t_end self._timings[key] = (t_start, t_end) return t_start, t_end
[docs] def get_full_gate_length(self): t_gate_start = np.inf t_gate_end = -np.inf for chan in self.comps: self._timings = dict() for name in self.comps[chan]: start, end = self.get_timings(chan, name, minimal_time=True) t_gate_start = min(t_gate_start, start) t_gate_end = max(t_gate_end, end) return t_gate_start, t_gate_end
[docs] def auto_adjust_t_end(self, buffer=0): while True: t_end = self.get_full_gate_length()[1] if self.t_end == t_end: break self.t_end = t_end self.t_end = float(t_end * (1 + buffer))
[docs] def get_awg_signal(self, chan, ts): amp_tot_sq = 0 signal = tf.zeros_like(ts, tf.complex128) self._timings = dict() for comp_name in self.comps[chan]: comp = self.comps[chan][comp_name] t_start, t_end = self.get_timings(chan, comp_name) ts_off = ts - t_start if isinstance(comp, Envelope): amp_re = comp.params["amp"].get_value() amp = tf.complex(amp_re, tf.zeros_like(amp_re)) amp_tot_sq += amp**2 xy_angle = comp.params["xy_angle"].get_value() freq_offset = comp.params["freq_offset"].get_value() phase = xy_angle - freq_offset * ts_off env = comp.get_shape_values(ts_off, t_end - t_start) env = tf.cast(env, tf.complex128) signal += ( amp * env * tf.math.exp(tf.complex(tf.zeros_like(phase), phase)) ) norm = tf.sqrt(tf.cast(amp_tot_sq, tf.float64)) inphase = tf.math.real(signal) quadrature = tf.math.imag(signal) return {"inphase": inphase, "quadrature": quadrature}, norm
[docs] def quick_setup(self, chan, qubit_freq, gate_time, v2hz=1, sideband=None) -> None: """ Initialize this instruction with a default envelope and carrier. """ pi_half_amp = np.pi / 2 / gate_time / v2hz * 2 * np.pi env_params = { "t_final": Quantity(value=gate_time, unit="s"), "amp": Quantity( value=pi_half_amp, min_val=0.0, max_val=3 * pi_half_amp, unit="V" ), } carrier_freq = qubit_freq if sideband: env_params["freq_offset"] = Quantity(value=sideband, unit="Hz 2pi") carrier_freq -= sideband self.add_component( comp=Envelope( "gaussian", shape=gaussian_nonorm, params=env_params, use_t_before=False ), chan=chan, ) self.add_component( comp=Carrier( "carrier", params={"freq": Quantity(value=carrier_freq, unit="Hz 2pi")} ), chan=chan, )