diff --git a/.flake8 b/.flake8 index 106dcb00..894c206a 100644 --- a/.flake8 +++ b/.flake8 @@ -3,3 +3,5 @@ max-line-length = 80 select = C,E,F,W,B,B950 ignore = E203, E501, W503, E741, B950, F405, F403 exclude = **/tests/** +per-file-ignores = + qualang_tools/config/components.py:E743 diff --git a/qualang_tools/config/__init__.py b/qualang_tools/config/__init__.py index 9ded8dbd..e102f211 100644 --- a/qualang_tools/config/__init__.py +++ b/qualang_tools/config/__init__.py @@ -2,5 +2,41 @@ convert_integration_weights, compress_integration_weights, ) +from qualang_tools.config.components import * +from qualang_tools.config.primitive_components import * +from qualang_tools.config.builder import * -__all__ = ["convert_integration_weights", "compress_integration_weights"] +__all__ = [ + "convert_integration_weights", + "compress_integration_weights", + "Controller", + "ArbitraryWaveform", + "ConstantWaveform", + "DigitalWaveform", + "MeasurePulse", + "ControlPulse", + "Mixer", + "Element", + "MeasureElement", + "ConstantIntegrationWeights", + "ArbitraryIntegrationWeights", + "ElementCollection", + "ReadoutResonator", + "Transmon", + "FluxTunableTransmon", + "Coupler", + "Oscillator", + "Port", + "AnalogInputPort", + "AnalogOutputPort", + "DigitalInputPort", + "DigitalOutputPort", + "Waveform", + "Pulse", + "Operation", + "IntegrationWeights", + "Weights", + "DigitalSample", + "Matrix2x2", + "AnalogOutputFilter", +] diff --git a/qualang_tools/config/builder.py b/qualang_tools/config/builder.py new file mode 100644 index 00000000..8da9af43 --- /dev/null +++ b/qualang_tools/config/builder.py @@ -0,0 +1,236 @@ +from typing import Union, Dict + +from qualang_tools.config.configuration import * +from qualang_tools.config.exceptions import ConfigurationError +from qualang_tools.config.components import * +from qualang_tools.config.parameters import isparametric + + +class ConfigBuilder: + def __init__(self) -> None: + """A class used to build the configuration of an experiment with QUA""" + self.objects = [] + self.configuration = QMConfiguration([]) + self.controllers = [] + self.components = [] + + def add(self, obj): + """Adds an entity to the configuraiton. + + :param obj: Any entity type supported by the configuration system, e.g. Element, ArbitraryWaveform or Transmon + :type obj: + """ + if isinstance(obj, list): + for _obj in obj: + self.objects.append(obj) + else: + self.objects.append(obj) + return self + + @property + def ports(self): + ports = [] + for cont in self.controllers: + ports += cont.ports + return ports + + def _add_controller(self, cont: Controller): + self.controllers.append(cont) + for port in cont.ports: + if isinstance(port, AnalogOutputPort): + if port.delay is not None: + cont.dict["analog_outputs"][port.info[1]]["delay"] = port.delay + if port.filter is not None: + cont.dict["analog_outputs"][port.info[1]]["filter"] = { + "feedback": port.filter.feedback, + "feedforward": port.filter.feedforward, + } + if port.channel_weights is not None: + cont.dict["analog_outputs"][port.info[1]][ + "channel_weights" + ] = port.channel_weights + elif isinstance(port, AnalogInputPort): + if port.gain_db is not None: + cont.dict["analog_inputs"][port.info[1]]["gain_db"] = port.gain_db + elif isinstance(port, DigitalOutputPort): + pass + elif isinstance(port, DigitalInputPort): + if port.polarity is not None: + cont.dict["digital_inputs"][port.info[1]][ + "polarity" + ] = port.polarity + if port.threshold is not None: + cont.dict["digital_inputs"][port.info[1]][ + "threshold" + ] = port.threshold + if port.window is not None: + cont.dict["digital_inputs"][port.info[1]]["window"] = port.window + + self.configuration.config["controllers"][cont.name] = cont.dict + + def _add_component(self, comp: ElementCollection): + self.components.append(comp) + for elm in comp.elements: + self.configuration.add_element(elm) + + def build(self, config: Dict = {}): + """Generates the QUA configuration represented by the current state of the builder object + + :param config: An initial QUA config, defaults to {} + :type config: Dict, optional + :return: QUA configuration + :rtype: Dict + """ + if config == {}: + self.configuration.reset() + self.controllers = [] + self.components = [] + else: + self.configuration.config = copy.deepcopy(config) + for obj in self.objects: + if isparametric(obj): + raise ConfigurationError("set the parameters of {0}".format(obj.name)) + if isinstance(obj, Controller): + self._add_controller(obj) + elif isinstance(obj, Waveform): + self.configuration.add_waveform(obj) + elif isinstance(obj, Pulse): + self.configuration.add_pulse(obj) + elif isinstance(obj, ElementCollection): + self._add_component(obj) + elif isinstance(obj, IntegrationWeights): + self.configuration.add_integration_weights(obj) + elif isinstance(obj, Mixer): + self.configuraiton.add_mixer(obj) + elif isinstance(obj, Oscillator): + self.configuration.add_oscillator(obj) + else: + raise NotImplementedError( + "Can not add objects of type {0}".format(type(obj)) + ) + return self.configuration.config + + def find_instances(self, obj_type): + """Find instances of a certain class in the configuration + + :param obj_type: The required type + :type obj_type: [type] + :return: A list of instances + :rtype: list + """ + return [elm for elm in self.objects if isinstance(elm, obj_type)] + + def find_users_of( + self, + elm: Union[ + AnalogInputPort, + AnalogOutputPort, + Controller, + Waveform, + Pulse, + Mixer, + Oscillator, + ], + ): + """Find where in the configuration a specific object is used + + :param elm: The element of interest + :type elm: Union[AnalogInputPort, AnalogOutputPort, Controller, Waveform, Pulse] + :return: A set of users + :rtype: Set + """ + if ( + isinstance(elm, AnalogInputPort) + or isinstance(elm, AnalogOutputPort) + or isinstance(elm, DigitalInputPort) + or isinstance(elm, DigitalOutputPort) + ): + return self._find_users_of_port(elm) + elif isinstance(elm, Controller): + return self._find_users_of_controller(elm) + elif isinstance(elm, Waveform): + return self._find_users_of_waveform(elm) + elif isinstance(elm, Pulse): + return self._find_users_of_pulse(elm) + elif isinstance(elm, IntegrationWeights): + return self._find_users_of_integration_weights(elm) + elif isinstance(elm, Mixer): + return self._find_users_of_mixer(elm) + else: + raise NotImplementedError( + "can not find objects of type {}".format(type(elm)) + ) + + def _find_users_of_port(self, port: Port): + objects = [] + for obj in self.objects: + if ( + isinstance(obj, Element) + or isinstance(obj, ElementCollection) + or isinstance(obj, MeasureElement) + ): + if port in obj.ports: + objects.append(obj) + return set(objects) + + def _find_users_of_controller(self, cont: Controller): + objects = [] + for obj in self.objects: + if isinstance(obj, Element) or isinstance(obj, MeasureElement): + for port in obj.ports: + if port in cont.ports: + objects.append(obj) + break + elif isinstance(obj, ElementCollection): + for _obj in obj.elements: + for port in _obj.ports: + if port in cont.ports: + objects.append(obj) + break + return set(objects) + + def _find_users_of_waveform(self, wf: Waveform): + objects = [] + for obj in self.objects: + if ( + isinstance(obj, Element) + or isinstance(obj, MeasureElement) + or isinstance(obj, ElementCollection) + ): + for name in obj.waveform_names: + if name == wf.name: + objects.append(obj) + return set(objects) + + def _find_users_of_pulse(self, elm: Pulse): + objects = [] + for obj in self.objects: + if ( + isinstance(obj, Element) + or isinstance(obj, MeasureElement) + or isinstance(obj, ElementCollection) + ): + for name in obj.pulse_names: + if name == elm.name: + objects.append(obj) + return set(objects) + + def _find_users_of_integration_weights(self, elm: IntegrationWeights): + objects = [] + for obj in self.objects: + if isinstance(obj, MeasurePulse): + for w in obj.integration_weights: + if w.name == elm.name: + objects.append(obj) + return set(objects) + + def _find_users_of_mixer(self, elm: Mixer): + objects = [] + for obj in self.objects: + if isinstance(obj, Oscillator): + if obj.mixer == elm.name: + objects.append(obj) + elif isinstance(obj, Element): + if obj.mixer.name == elm.name: + objects.append(obj) + return set(objects) diff --git a/qualang_tools/config/components.py b/qualang_tools/config/components.py new file mode 100644 index 00000000..1ec3221c --- /dev/null +++ b/qualang_tools/config/components.py @@ -0,0 +1,949 @@ +from typing import Union, List, Tuple + +from qualang_tools.config.primitive_components import * +from qualang_tools.config.exceptions import ConfigurationError + + +class Controller: + def __init__( + self, + name: str, + n_analog_outputs: int = 0, + n_analog_inputs: int = 0, + n_digital_outputs: int = 0, + n_digital_inputs: int = 0, + controller_type: str = "opx1", + ): + """A QOP controller + + :param name: Name for this controller + :type name: str + :param n_analog_outputs: Number of analog outputs defined at initialization, defaults to 0 + :type n_analog_outputs: int, optional + :param n_analog_inputs: Number of analog inputs defined at initialization, defaults to 0 + :type n_analog_inputs: int, optional + :param n_digital_outputs: Number of digital outputs defined at initialization, defaults to 0 + :type n_digital_outputs: int, optional + :param n_digital_inputs: Number of digital inputs defined at initialization, defaults to 0 + :type n_digital_inputs: int, optional + :param controller_type: defaults to "opx1" + :type controller_type: str, optional + """ + self.name = name + self.dict = dict() + self.dict["type"] = controller_type + self.dict["analog_outputs"] = { + i: {"offset": 0} for i in range(1, n_analog_outputs + 1) + } + self.dict["analog_inputs"] = { + i: {"offset": 0} for i in range(1, n_analog_inputs + 1) + } + self.dict["digital_outputs"] = { + i: {"offset": 0} for i in range(1, n_digital_outputs + 1) + } + self.dict["digital_inputs"] = { + i: {"offset": 0} for i in range(1, n_digital_inputs + 1) + } + self.analog_input_ports = [ + AnalogInputPort(name, i) for i in range(1, n_analog_inputs + 1) + ] + self.analog_output_ports = [ + AnalogOutputPort(name, i) for i in range(1, n_analog_outputs + 1) + ] + self.digital_output_ports = [ + DigitalOutputPort(name, i) for i in range(1, n_digital_outputs + 1) + ] + self.digital_input_ports = [ + DigitalInputPort(name, i) for i in range(1, n_digital_inputs + 1) + ] + + def analog_output(self, port: int, offset: float = 0): + """Returns an instance of AnalogOutputPort associated with a specific port number and offset if already in the configuration. + otherwise, opens a new instance with the given port number. + + :param port: physical output port numebr + :type port: int + :param offset: sets the voltage offset of that port, defaults to 0 + :type offset: float, optional + :return: An instance of the analog port + :rtype: AnalogOutputPort + """ + for (i, p) in enumerate(self.analog_output_ports): + if port == p.info[1]: + p.offset = offset + return p + self.use_analog_output_port(port, offset) + return self.analog_output_ports[-1] + + def analog_input(self, port: int, offset: float = 0): + """Returns an instance of AnalogInputPort with a specific port number and offset if already in the configuration. + otherwise, opens a new instance with the given port number and offset. + + :param port: physical input port numebr + :type port: int + :param offset: [description], defaults to 0 + :type offset: float, optional + :return: An instance of the input port + :rtype: AnalogInputPort + """ + for (i, p) in enumerate(self.analog_input_ports): + if port == p.info[1]: + p.offset = offset + return p + self.use_analog_input_port(port, offset) + return self.analog_input_ports[-1] + + def digital_output(self, port: int, offset: float = 0): + """Returns an instance of DigitalOutputPort with a specific port number and offset if already in the configuration. + otherwise, opens a new instance with the given port number and offset. + + :param port: port number in the range 1-10 + :type port: int + :param offset: defaults to 0 + :type offset: float, optional + :return: + :rtype: DigitalOutputPort + """ + for (i, p) in enumerate(self.digital_output_ports): + if port == p.info[1]: + p.offset = offset + return p + self.use_digital_output_port(port, offset) + return self.digital_output_ports[-1] + + def digital_input(self, port: int, offset: float = 0): + + for (i, p) in enumerate(self.digital_ports): + if port == p.info[1]: + p.offset = offset + return p + self.use_digital_input_port(port, offset) + return self.digital_input_ports[-1] + + def __contains__(self, port) -> bool: + for p in self.input_ports + self.output_ports + self.digital_ports: + if port == p: + return True + return False + + def use_analog_input_port(self, port_num: int, offset: float = 0.0): + """Adds an instance of AnalogInputPort to configuration if it is not used + + :param port_num: port number + :type port_num: int + :param offset: voltage offset for this port, defaults to 0.0 + :type offset: float, optional + """ + if port_num not in self.dict["analog_inputs"]: + self.dict["analog_inputs"][port_num] = {"offset": offset} + self.analog_input_ports += [ + AnalogInputPort(self.name, port_num, offset=offset) + ] + + def use_analog_output_port(self, port_num: int, offset: float = 0.0): + """Adds an instance of AnalogOutputPort to configuration if it is not used + + :param port_num: port number + :type port_num: int + :param offset: voltage offset for this port, defaults to 0.0 + :type offset: float, optional + """ + if port_num not in self.dict["analog_outputs"]: + self.dict["analog_outputs"][port_num] = {"offset": offset} + self.analog_output_ports += [ + AnalogOutputPort(self.name, port_num, offset=offset) + ] + + def use_digital_output_port(self, port_num: int, offset: float = 0.0): + """Adds an instance of DigitalOutputPort to configuration if it is not used + + :param port_num: port number + :type port_num: int + :param offset: defaults to 0.0 + :type offset: float, optional + """ + if port_num not in self.dict["digital_outputs"]: + self.dict["digital_outputs"][port_num] = {"offset": offset} + self.digital_output_ports += [ + DigitalOutputPort(self.name, port_num, offset=offset) + ] + + def use_digital_input_port(self, port_num: int, offset: float = 0.0): + """Adds an instance of DigitalInputPort to configuration if it is not used + + :param port_num: port number + :type port_num: int + :param offset: defaults to 0.0 + :type offset: float, optional + """ + if port_num not in self.dict["digital_inputs"]: + self.dict["digital_inputs"][port_num] = {"offset": offset} + self.digital_input_ports += [ + DigitalInputPort(self.name, port_num, offset=offset) + ] + + @property + def ports(self): + """Returns the set of all used ports in this configuration + + :return: + :rtype: Set + """ + return ( + self.digital_input_ports + + self.digital_output_ports + + self.analog_input_ports + + self.analog_output_ports + ) + + +class ArbitraryWaveform(Waveform): + def __init__(self, name: str, samples: List[float]): + """An arbitrary waveform played by the QOP + + :param name: name of the played WF + :type name: str + :param samples: list of samples to be played (in the range -.5 to .5), specified at 1 sample per nanosecond + :type samples: List[float] + """ + super().__init__(name, {"type": "arbitrary", "samples": samples}) + + +class ConstantWaveform(Waveform): + def __init__(self, name: str, sample: float): + """A constant amplitude WF + + :param name: name of the WF + :type name: str + :param sample: Amplitude of the WF in the range -.5 to .5 + :type sample: float + """ + super().__init__(name, {"type": "constant", "sample": sample}) + + +class DigitalWaveform: + def __init__(self, name: str, samples: List[DigitalSample]): + """A ditial waveform played from a digital output + + :param name: The name of the digital waveform, defaults to "ON" + :type name: str + :param samples: The digital waveforms, specified in the format [(1/0, dur),...] where dur is in nanoseconds. defaults to [(1, 0)] + :type samples: List[DigitalSample] + """ + self.name = name + self.dict = dict() + if isinstance(samples[0], DigitalSample): + self._samples = samples + else: + self._samples = [ + DigitalSample(state, duration) for (state, duration) in samples + ] + self.dict["samples"] = [(ds.state, ds.duration) for ds in self._samples] + + @property + def samples(self): + return self._samples + + @samples.setter + def samples(self, samples: Union[List[Tuple[int, int]], List[DigitalSample]]): + if isinstance(samples[0], DigitalSample): + self._samples = samples + else: + self._samples = [ + DigitalSample(state, duration) for (state, duration) in samples + ] + self.dict["samples"] = [(ds.state, ds.duration) for ds in self._samples] + + +class MeasurePulse(Pulse): + def __init__(self, name: str, wfs: List[Waveform], length: int): + """A pulse which can be used by a QUA measure() command + + :param name: name for this pulse + :type name: str + :param wfs: waveforms to be played during measurement + :type wfs: List[Waveform] + :param length: duration of the measurment pulse + :type length: int, optional + """ + super().__init__(name, wfs, "measure", length) + self.digital_markers = [] + self.integration_weights = [] + + def add( + self, + objs: Union[Weights, DigitalWaveform, List[Union[Weights, DigitalWaveform]]], + ): + if isinstance(objs, list): + for obj in objs: + self._add(obj) + else: + self._add(objs) + return self + + def _add(self, w: Union[Weights, DigitalWaveform]): + if isinstance(w, Weights): + if isinstance(w.weights, ConstantIntegrationWeights): + assert w.weights.dict["cosine"][0][1] == self.dict["length"] + else: + assert len(w.weights.dict["sine"]) == self.dict["length"] // 4 + self.integration_weights.append(w) + elif isinstance(w, DigitalWaveform): + self.digital_markers.append(w) + + +class ControlPulse(Pulse): + def __init__(self, name: str, wfs: List[Waveform], length: int): + """A pulse which can be used by a QUA play() command + :return: + :rtype: ControlPulse + """ + super().__init__(name, wfs, "control", length) + + +class Mixer: + def __init__( + self, + name: str, + intermediate_frequency: int, + lo_frequency: int, + correction: Matrix2x2, + ): + """A microwave mixer + + :param name: name for this mixer + :type name: str + :param intermediate_frequency: intermediate_frequency in MHz + :type intermediate_frequency: int + :param lo_frequency: local oscillator frequency in MHz + :type lo_frequency: int + :param correction: correction matrix for this mixer + :type correction: Matrix2x2 + """ + self.name = name + self._correction = correction + self.dict = dict() + self.dict["intermediate_frequency"] = intermediate_frequency + self.dict["lo_frequency"] = lo_frequency + self.dict["correction"] = tuple(correction.data[0] + correction.data[1]) + + @property + def intermediate_frequency(self): + return self.dict["intermediate_frequency"] + + @intermediate_frequency.setter + def intermediate_frequency(self, if_freq: int): + self.dict["intermediate_frequency"] = if_freq + + @property + def lo_frequency(self): + return self.dict["lo_frequency"] + + @lo_frequency.setter + def lo_frequency(self, lo_freq: int): + self.dict["lo_frequency"] = lo_freq + + @property + def correction(self): + return self._correction + + @correction.setter + def correction(self, correction: Matrix2x2): + self._correction = correction + self.dict["correction"] = tuple(correction.data[0] + correction.data[1]) + + +class Element: + def __init__( + self, + name: str, + analog_output_ports: List[AnalogOutputPort], + analog_input_ports: List[AnalogInputPort] = [], + digital_output_ports: List[DigitalOutputPort] = [], + digital_input_ports: List[DigitalInputPort] = [], + intermediate_frequency: int = None, + lo_frequency: int = None, + ): + """A quantum element in a configuration + + :param name: element name + :type name: int + :param analog_output_ports: AnalogOutput(s) associated with this element + :type analog_output_ports: List[AnalogOutputPort] + :param analog_input_ports: AnalogInputPort(s) associated with this element, defaults to [] + :type analog_input_ports: List[AnalogInputPort], optional + :param digital_output_ports: DigitalOutputPort(s) associated with this element, defaults to [] + :type digital_output_ports: List[DigitalOutputPort], optional + :param intermediate_frequency: intermediate frequency associated wit this element, defaults to None + :type intermediate_frequency: int, optional + :param lo_frequency: LO frequency associated wit this element, defaults to None + :type lo_frequency: int, optional + """ + self.name = name + self.dict = dict() + assert len(analog_output_ports) <= 2 + self.type = "singleInput" if len(analog_output_ports) == 1 else "mixInputs" + self.pulses = [] + self.analog_input_ports = analog_input_ports + self.analog_output_ports = analog_output_ports + self.digital_output_ports = digital_output_ports + self.digital_input_ports = digital_input_ports + self.mixer = None + if self.type == "singleInput": + port = analog_output_ports[0] + self.dict["singleInput"] = dict() + self.dict["singleInput"]["port"] = port.info + elif self.type == "mixInputs": + I = analog_output_ports[0] + Q = analog_output_ports[1] + self.dict["mixInputs"] = dict() + self.dict["mixInputs"]["I"] = I.info + self.dict["mixInputs"]["Q"] = Q.info + self.dict["mixInputs"]["lo_frequency"] = lo_frequency + self.dict["outputs"] = dict() + for i, port in enumerate(analog_input_ports): + self.dict["outputs"]["out" + str(i + 1)] = port.info + self.dict["operations"] = dict() + self.dict["intermediate_frequency"] = intermediate_frequency + + def _add_mixer(self, mix: Mixer): + assert "mixInputs" in self.dict.keys() + self.dict["mixInputs"]["mixer"] = mix.name + self.mixer = mix + + @property + def lo_frequency(self): + assert "mixInputs" in self.dict.keys() + return self.dict["mixInputs"]["lo_frequency"] + + @lo_frequency.setter + def lo_frequency(self, lo_frequency: int): + assert "mixInputs" in self.dict.keys() + self.dict["mixInputs"]["lo_frequency"] = lo_frequency + + @property + def intermediate_frequency(self): + return self.dict["intermediate_frequency"] + + @intermediate_frequency.setter + def intermediate_frequency(self, omega_IF: int): + """Set the IF associated with this element + + :param omega_IF: + :type omega_IF: int + """ + assert self.type == "mixInputs" + self.dict["intermediate_frequency"] = omega_IF + + def _add_operation(self, op: Operation): + self.dict["operations"][op.name] = op.pulse.name + self.pulses.append(op.pulse) + + def _add(self, obj: Union[Operation, Mixer]): + """A method to add components to an element + + :param obj: The component to be added + :type obj: [type] + :raises ConfigurationError: [description] + """ + if isinstance(obj, Operation): + self._add_operation(obj) + elif isinstance(obj, Mixer): + self._add_mixer(obj) + else: + raise ConfigurationError("Adding unsupported object") + + def add(self, objs: Union[Operation, Mixer, List[Union[Operation, Mixer]]]): + if isinstance(objs, list): + for obj in objs: + self._add(obj) + else: + self._add(objs) + return self + + @property + def operation_names(self): + return self.dict["operations"].keys() + + @property + def pulse_names(self): + return set([p.name for p in self.pulses]) + + @property + def waveform_names(self): + names = [] + for pl in self.pulses: + names += pl.waveform_names + return set(names) + + @property + def ports(self): + return ( + self.analog_input_ports + + self.analog_output_ports + + self.digital_output_ports + + self.digital_input_ports + ) + + +class MeasureElement(Element): + def __init__( + self, + name: str, + analog_output_ports: List[AnalogOutputPort], + analog_input_ports: List[AnalogInputPort], + digital_output_ports: List[DigitalOutputPort] = [], + intermediate_frequency: int = None, + lo_frequency: int = None, + ): + """A quantum element set for performing measurment + + :param name: [description] + :type name: int + :param analog_output_ports: [description] + :type analog_output_ports: List[AnalogOutputPort] + :param analog_input_ports: [description] + :type analog_input_ports: List[AnalogInputPort] + :param digital_output_ports: [description], defaults to [] + :type digital_output_ports: List[DigitalOutputPort], optional + :param intermediate_frequency: intermediate frequency associated wit this element, defaults to None + :type intermediate_frequency: int, optional + :param lo_frequency: [description], defaults to None + :type lo_frequency: int, optional + """ + super().__init__( + name, analog_output_ports, analog_input_ports, digital_output_ports + ) + self.dict["time_of_flight"] = 0 + self.dict["smearing"] = 0 + self.dict["intermediate_frequency"] = intermediate_frequency + + @property + def time_of_flight(self): + return self.dict["time_of_flight"] + + @time_of_flight.setter + def time_of_flight(self, tof: int): + self.dict["time_of_flight"] = tof + + @property + def smearing(self): + return self.dict["smearing"] + + @smearing.setter + def smearing(self, smearing: int): + self.dict["smearing"] = smearing + + +class ConstantIntegrationWeights(IntegrationWeights): + def __init__(self, name: str, cosine: float, sine: float, duration: int): + """A constant integration weight used in integration and demodulation + :param name: name for the weights vector + :type name: str + :param cosine: value of the cosine vector + :type cosine: float + :param sine: value of the cosine vector + :type sine: float + :param duration: duration of the read out pulse + :type duration: int + """ + self._duration = duration + super().__init__(name, [(cosine, duration)], [(sine, duration)]) + + @property + def cosine(self): + return self.dict["cosine"] + + @cosine.setter + def cosine(self, v: float): + self.dict["cosine"] = [(v, self._duration)] + + @property + def sine(self): + return self.dict["sine"] + + @sine.setter + def sine(self, v: float): + self.dict["sine"] = [(v, self._duration)] + + +class ArbitraryIntegrationWeights(IntegrationWeights): + def __init__(self, name: str, cosine: List[float], sine: List[float]): + """A vector of integration weights used in integration and demodulation + :param name: name for the weights vector + :type name: str + :param cosine: a list of weights to be used with the cosine demodulation element, specified at 1 sample per 4 nanoseconds + :type cosine: List + :param sine: a list of weights to be used with the sine demodulation element, specified at 1 sample per 4 nanoseconds + :type sine: List + """ + assert len(sine) == len(cosine) + super().__init__(name, cosine, sine) + + @property + def cosine(self): + return self.dict["cosine"] + + @cosine.setter + def cosine(self, w: List[float]): + self.dict["cosine"] = w + + @property + def sine(self): + return self.dict["sine"] + + @sine.setter + def sine(self, w: List[float]): + self.dict["sine"] = w + + +class ElementCollection: + def __init__(self, name: str, elements: List[Element] = []): + """A named collection of Elements + + It also a useful base class for building components. + """ + self.name = name + self.elements = elements + + @property + def ports(self): + ports = [] + for elm in self.elements: + ports += elm.ports + return ports + + @property + def waveform_names(self): + names = [] + for elm in self.elements: + names += elm.waveform_names + return names + + @property + def operation_names(self): + names = [] + for elm in self.elements: + names += elm.operation_names + return names + + @property + def pulse_names(self): + names = [] + for elm in self.elements: + names += elm.pulse_names + return names + + +class ReadoutResonator(ElementCollection): + def __init__( + self, + name: str, + outputs: List[AnalogOutputPort], + inputs: List[AnalogInputPort], + intermediate_frequency: int, + ): + """ + A Microwave readout resonator + + :param name: A name for this resonator + :type name: str + :param outputs: A pair of output ports + :type outputs: List[AnalogOutputPort] + :param inputs: A pair of input ports + :type inputs: List[AnalogInputPort] + :param intermediate_frequency: The intermediate frequency + :type intermediate_frequency: int + """ + if len(outputs) != 2: + raise ConfigurationError("Number of output ports is not equal to 2") + if len(inputs) != 2: + raise ConfigurationError("Number of input ports is not equal to 2") + drive = MeasureElement( + name, outputs, inputs, intermediate_frequency=intermediate_frequency + ) + super().__init__(name=name, elements=[drive]) + + @property + def intermediate_frequency(self): + return self.elements[0].intermediate_frequency + + @intermediate_frequency.setter + def intermediate_frequency(self, if_freq: int): + """Set an IF for the resonator + + :param if_freq: A frequency in MHz + :type if_freq: int + """ + self.elements[0].intermediate_frequency = if_freq + + def add(self, obj: Union[Operation, List[Operation]]): + """Add list of operations associated with the Transmon + + :param obj: The list of operations + :type obj: list or an instance of Operation + """ + if isinstance(obj, list): + for o in obj: + self._add(o) + else: + self._add(obj) + return self + + def _add(self, op: Operation): + """Add operation associated with the Readout resonator + + :param obj: The operation + :type obj: Operation + + """ + if len(op.pulse.wfs) == 2: + self.elements[0].add(op) + else: + raise ConfigurationError("adding unsupported object") + + @property + def time_of_flight(self): + return self.elements[0].time_of_flight + + @time_of_flight.setter + def time_of_flight(self, tof: int): + self.elements[0].time_of_flight = tof + + @property + def smearing(self): + return self.elements[0].smearing + + @smearing.setter + def smearing(self, t: int): + self.elements[0].smearing = t + + @property + def lo_frequency(self): + return self.elements[0].lo_frequency + + @lo_frequency.setter + def lo_frequency(self, lo_frequency: int): + self.elements[0].lo_frequency = lo_frequency + + @property + def input_ports(self): + return self.elements[0].analog_input_ports + + @input_ports.setter + def input_ports(self, ports: List[AnalogInputPort]): + assert len(ports) == 2 + self.elements[0].analog_input_ports = ports + for i, port in enumerate(ports): + self.elements[0].dict["outputs"]["out" + str(i)] = port.info + + @property + def output_ports(self): + return self.elements[0].analog_output_ports + + @output_ports.setter + def output_ports(self, ports: List[AnalogOutputPort]): + assert len(ports) == 2 + self.elements[0].analog_output_ports = ports + self.elements[0].dict["mixInputs"]["I"] = ports[0].info + self.elements[0].dict["mixInputs"]["Q"] = ports[1].info + + @property + def mixer(self): + return self.elements[0].mixer + + @mixer.setter + def mixer(self, mixer: Mixer): + self.elements[0].mixer = mixer + self.elements[0].dict["mixInputs"]["mixer"] = mixer.name + + +class Transmon(ElementCollection): + def __init__( + self, + name: str, + I: AnalogOutputPort, + Q: AnalogOutputPort, + intermediate_frequency: int, + ): + """A superconducting transmon qubit + + :param name: A name for this transmon + :type name: str + :param I: I output port + :type I: AnalogOutputPort + :param Q: Q output port + :type Q: AnalogOutputPort + :param intermediate_frequency: intermediate frequency of the upconversion IQ mixer in MHz + :type intermediate_frequency: int + """ + drive = Element(name, [I, Q], intermediate_frequency=intermediate_frequency) + super().__init__(name=name, elements=[drive]) + + @property + def I(self): + return self.elements[0].output_ports[0] + + @I.setter + def I(self, p: AnalogOutputPort): + self.elements[0].output_ports[0] = p + self.elements[0].dict["mixInputs"]["I"] = p.info + + @property + def Q(self): + return self.elements[0].output_ports[1] + + @Q.setter + def Q(self, p: AnalogOutputPort): + self.elements[0].output_ports[1] = p + self.elements[0].dict["mixInputs"]["Q"] = p.info + + @property + def lo_frequency(self): + return self.elements[0].lo_frequency + + @lo_frequency.setter + def lo_frequency(self, lo_frequency: int): + self.elements[0].lo_frequency = lo_frequency + + @property + def intermediate_frequency(self): + return self.elements[0].intermediate_frequency + + @intermediate_frequency.setter + def intermediate_frequency(self, if_freq: int): + """Set an IF for the transmon + + :param if_freq: A frequency in MHz + :type if_freq: int + """ + self.elements[0].intermediate_frequency = if_freq + + def add(self, op: Union[Operation, List[Operation]]): + """Add list of operations associated with the Transmon + + :param obj: The operations + :type obj: list or an instance of Operation + """ + if isinstance(op, list): + for o in op: + self._add(o) + else: + self._add(op) + return self + + def _add(self, op: Operation): + if len(op.pulse.wfs) == 2: + self.elements[0].add(op) + else: + raise ConfigurationError("adding unsupported object") + + @property + def mixer(self): + return self.elements[0].mixer + + @mixer.setter + def mixer(self, mixer: Mixer): + self.elements[0].mixer = mixer + self.elements[0].dict["mixInputs"]["mixer"] = mixer.name + + +class FluxTunableTransmon(Transmon): + def __init__( + self, + name: str, + I: AnalogOutputPort, + Q: AnalogOutputPort, + fl_port: AnalogOutputPort, + intermediate_frequency: int, + ): + """A flux tuneable superconducting transmon qubit + + :param name: A name for this transmon + :type name: str + :param I: I output port + :type I: AnalogOutputPort + :param Q: Q output port + :type Q: AnalogOutputPort + :param fl_port: Flux line output Port + :type fl_port: AnalogOutputPort + :param intermediate_frequency: intermediate frequency of the upconversion IQ mixer in MHz + :type intermediate_frequency: int + """ + super().__init__(name, I, Q, intermediate_frequency) + self.elements.append(Element(name + "_flux_line", [fl_port])) + + def _add(self, op: Operation): + if len(op.pulse.wfs) == 2: + self.elements[0].add(op) + elif len(op.pulse.wfs) == 1: + self.elements[1].add(op) + else: + raise ConfigurationError( + "Can not add a pulse with {0} waveforms".format(len(op.pulse.wfs)) + ) + + +class Coupler(ElementCollection): + def __init__(self, name: str, p: AnalogOutputPort): + coupler = Element(name, analog_output_ports=[p]) + super().__init__(name=name, elements=[coupler]) + + def _add(self, op: Operation): + if len(op.pulse.wfs) == 1: + self.elements[0].add(op) + else: + raise ConfigurationError( + "Can not add a pulse with {0} waveforms".format(len(op.pulse.wfs)) + ) + + def add(self, op: Union[List[Operation], Operation]): + if isinstance(op, list): + for o in op: + self._add(o) + else: + self._add(op) + return self + + @property + def p(self): + return self.elements[0].analog_output_ports[0] + + @p.setter + def p(self, p: AnalogOutputPort): + self.elements[0].analog_output_ports[0] = p + + +class Oscillator: + def __init__( + self, name: str, intermediate_frequency: int, lo_frequency: int, mixer: str + ): + self.name = name + self.dict = dict() + self.dict["intermediate_frequency"] = intermediate_frequency + self.dict["lo_frequency"] = lo_frequency + self.dict["mixer"] = mixer + + @property + def lo_frequency(self): + return self.dict["lo_frequency"] + + @lo_frequency.setter + def lo_frequency(self, lo_frequency: int): + self.dict["lo_frequency"] = lo_frequency + + @property + def intermediate_frequency(self): + return self.dict["intermediate_frequency"] + + @intermediate_frequency.setter + def intermediate_frequency(self, intermediate_frequency: int): + self.dict["intermediate_frequency"] = intermediate_frequency + + @property + def mixer(self): + return self.dict["mixer"] + + @mixer.setter + def mixer(self, mixer: str): + self.dict["mixer"] = mixer diff --git a/qualang_tools/config/configuration.py b/qualang_tools/config/configuration.py new file mode 100644 index 00000000..1f2d165a --- /dev/null +++ b/qualang_tools/config/configuration.py @@ -0,0 +1,221 @@ +import copy +from typing import List, Dict + +from qualang_tools.config.primitive_components import * +from qualang_tools.config.components import * +from qualang_tools.config.parameters import * + + +class QMConfiguration: + def __init__(self, controllers: List[Controller] = None, version: int = 1): + """A class to generate the QM configuration + + :param controllers: A controller or list of controllers used in this configuration, defaults to None + :type controllers: List[Controller], optional + :param version: Controller version, defaults to 1 + :type version: int, optional + """ + self.config = dict() + self.config["version"] = 1 + self.config["controllers"] = dict() + for cont in controllers: + self.config["controllers"][cont.name] = cont.dict + self.config["elements"] = dict() + self.config["pulses"] = dict() + self.config["waveforms"] = dict() + self.config["digital_waveforms"] = dict() + self.config["integration_weights"] = dict() + self.config["mixers"] = dict() + self.config["oscillators"] = dict() + + def add_waveform(self, wf: Waveform): + """Add a single waveform to this configuration + + :param wf: a Wavefrom object + :type wf: Waveform + """ + self.config["waveforms"][wf.name] = self._call_dict_parameters(wf.dict) + + def _call_dict_parameters(self, dic: Dict): + if iscallable(dic): + if not isparametric(dic): + _dic = copy.deepcopy(dic) + for (k, v) in _dic.items(): + if iscallable(v): + _dic[k] = v() + return _dic + return dic + + def add_waveforms(self, wfs: List[Waveform]): + """Add a list of waveforms to this configuration + + :param wfs: a list of Waveform objects + :type wfs: List[Waveform] + """ + for wf in wfs: + self.add_waveform(wf) + + def add_pulse(self, pulse: Pulse): + """Add a pulse to this configuration + + :param pulse: a Pulse object + :type pulse: Pulse + """ + _dict = copy.deepcopy(pulse.dict) + self.add_waveforms(pulse.wfs) + if isinstance(pulse, MeasurePulse): + _dict["digital_marker"] = {} + for marker in pulse.digital_markers: + _dict["digital_marker"] = {marker.name: marker.name} + self.config["digital_waveforms"][marker.name] = marker.dict + _dict["integration_weights"] = dict() + for w in pulse.integration_weights: + _dict["integration_weights"][w.name] = w.weights.name + self.config["integration_weights"][w.weights.name] = w.weights.dict + self.config["pulses"][pulse.name] = _dict + + def add_pulses(self, pulses: List[Pulse]): + """Add a list of Pulses to this configuration + + :param pulses: a list of Pulse objects + :type pulses: List[Pulse] + """ + for pulse in pulses: + self.add_pulse(pulse) + + def add_element(self, elm: Element): + """Add a quantum element to this configuration + + :param elm: an object of type Element + :type elm: Element + """ + # check if input/output ports belong to the controller + self.config["elements"][elm.name] = elm.dict + if elm.type == "mixInputs": + if elm.mixer is not None: + self.config["mixers"][elm.mixer.name] = elm.mixer.dict + self.add_pulses(elm.pulses) + + def update_pulse(self, elm_name: str, pulse: Pulse): + """Change the pulse associated with an element to the pulse given + + :param elm_name: name of a quantum element in the configuration + :type elm_name: str + :param pulse: a Pulse object + :type pulse: Pulse + """ + assert elm_name in self.config["elements"].keys() + self.config["elements"][elm_name]["operations"][pulse.name] = pulse.name + self._add_pulse_to_config(pulse) + + def update_integration_weights(self, pulse_name: str, iw: IntegrationWeights): + """Updates the integration_weights associated with a measurement pulse + + :param pulse_name: name of the measurement pulse + :type pulse_name: str + :param iw: IntegrationWeights object + :type iw: IntegrationWeights + """ + assert pulse_name in self.config["pulses"].keys() + assert self.config["pulses"][pulse_name]["operation"] == "measure" + assert len(self.config["pulses"][pulse_name]["waveforms"].keys()) == 2 + weight_name = pulse_name + "_weight" + self.config["integration_weights"][weight_name] = iw.dict + + def update_constant_waveform(self, wf_name: str, amp: float): + """Update the amplitude of an existing constant waveform + + :param wf_name: name of the existing waveform + :type wf_name: str + :param amp: amplitude to set in the range (-0.5 to 0.5) + :type amp: float + """ + assert wf_name in self.config["waveforms"].keys() + assert self.config["waveforms"][wf_name]["type"] == "constant" + self.config["waveforms"][wf_name]["sample"] = amp + + def update_arbitrary_waveform(self, wf_name: str, samples: List[float]): + """Update the samples of an existing arbitrary waveform + + :param wf_name: name of the existing waveform + :type wf_name: str + :param samples: a vector of samples + :type samples: List[float] + """ + assert wf_name in self.config["waveforms"].keys() + assert self.config["waveforms"][wf_name]["type"] == "arbitrary" + self.config["waveforms"][wf_name]["samples"] = samples + + def reset(self, controllers: List[Controller] = []): + self.__init__(controllers) + + def update_intermediate_frequency( + self, elm_name: str, freq: float, update_mixer: bool = True + ): + self.config["elements"][elm_name]["intermediate_frequency"] = freq + if update_mixer: + m_name = self.config["elements"][elm_name]["mixer"] + self.config["mixers"][m_name]["intermediate_frequency"] = freq + + def get_waveforms_from_op(self, elm_name, op_name): + """Get waveforms associated with an operation + + :param elm_name: name of the element + :type elm_name: [type] + :param op_name: name of the operation + :type op_name: [type] + + """ + assert elm_name in self.config["elements"].keys() + assert op_name in self.config["elements"]["operations"].keys() + waveforms = self.config["pulses"][op_name]["waveforms"] + if waveforms.keys() == ["single"]: + wf = waveforms["single"] + _dict = self.config["waveforms"][wf] + return [self.waveform_from_dict(wf, _dict)] + else: + wfI, wfQ = waveforms["I"], waveforms["Q"] + _dictI = self.config["waveforms"][wfI] + _dictQ = self.config["waveforms"][wfQ] + return [ + self.waveform_from_dict(wfI, _dictI), + self.waveform_from_dict(wfQ, _dictQ), + ] + + def waveform_from_dict(self, wf, _dict): + if _dict["type"] == "constant": + return ConstantWaveform(wf, _dict["sample"]) + else: + return ArbitraryWaveform(wf, _dict["samples"]) + + def add_controller(self, cont: Controller): + """Add a controller to this configuration + + :param cont: A Controller object + :type cont: Controller + """ + self.config["controllers"][cont.name] = cont.dict + + def add_integration_weights(self, weight: IntegrationWeights): + """Add integration weights to this configuration + + :param weight: an IntegrationWeights object + :type weight: IntegrationWeights + """ + self.config["integration_weights"][weight.name] = weight.dict + + def add_mixer(self, mixer: Mixer): + """Add a mixer to this configuration + + :param mixer: A Mixer object + :type mixer: Mixer + """ + self.config["mixers"][mixer.name] = mixer.dict + + def add_oscillator(self, oscillator: Oscillator): + """Add an oscillator to this configuration + + :param oscillator: an Oscillator object + :type oscillator: Oscillator + """ + self.config["oscillators"][oscillator.name] = oscillator.dict diff --git a/qualang_tools/config/exceptions.py b/qualang_tools/config/exceptions.py new file mode 100644 index 00000000..8021dafd --- /dev/null +++ b/qualang_tools/config/exceptions.py @@ -0,0 +1,4 @@ +class ConfigurationError(Exception): + def __init__(self, message) -> None: + self.message = message + super().__init__(self.message) diff --git a/qualang_tools/config/parameters.py b/qualang_tools/config/parameters.py new file mode 100644 index 00000000..0ea47b80 --- /dev/null +++ b/qualang_tools/config/parameters.py @@ -0,0 +1,62 @@ +class ConfigVar: + def __init__(self): + self.values = {} + self.value_set = {} + + def parameter(self, name: str): + if name in self.values: + return lambda: self.get(name) + if name.find(" ") != -1: + raise ValueError( + "Parameter name cannot contain spaces. " "Use underscore of camelCase." + ) + self.values[name] = None + self.value_set[name] = False + return lambda: self.get(name) + + def get(self, name: str): + assert self.value_set[name], f"Parameter '{name}' has not been defined yet." + return self.values[name] + + def set(self, **kwargs): + for key, value in kwargs.items(): + if key not in self.values: + self.parameter(key) + self.values[key] = value + self.value_set[key] = True + return + + +def isparametric(obj): + if callable(obj): + try: + obj() + return False + except AssertionError: + return True + elif hasattr(obj, "__dict__"): + for elm in obj.__dict__: + _obj = getattr(obj, elm) + if hasattr(_obj, "__dict__"): + return isparametric(_obj) + elif type(_obj) is dict: + for (k, v) in _obj.items(): + if isparametric(v): + return True + elif type(_obj) is list: + for elm in _obj: + if isparametric(elm): + return True + else: + return False + + +def iscallable(obj): + if callable(obj): + return True + elif type(obj) is dict: + for (k, v) in obj.items(): + if callable(v): + return True + else: + return False diff --git a/qualang_tools/config/primitive_components.py b/qualang_tools/config/primitive_components.py new file mode 100644 index 00000000..f54a706e --- /dev/null +++ b/qualang_tools/config/primitive_components.py @@ -0,0 +1,163 @@ +from typing import Any, Dict, List + + +class Port: + def __init__(self, controller: str, port_id: int, offset: float = 0) -> None: + self.info = (controller, port_id) + self.offset = offset + + def __eq__(self, other) -> bool: + return self.info == other.info and type(self) == type(other) + + def __str__(self): + cont, port_id = self.info + return ( + type(self).__name__ + + "(" + + str(cont) + + " , " + + str(port_id) + + ", offset = " + + str(self.offset) + + ")" + ) + + +class AnalogInputPort(Port): + def __init__(self, controller: str, port_id: int, offset: float = 0): + self.gain_db = None + super().__init__(controller, port_id, offset) + + +class AnalogOutputPort(Port): + def __init__(self, controller: str, port_id: int, offset: float = 0): + self.delay = None + self.filter = None + self.channel_weights = None + super().__init__(controller, port_id, offset) + + +class DigitalInputPort(Port): + def __init__(self, controller: str, port_id: int, offset: float = 0): + self.polarity = None + self.window = None + self.threshold = None + super().__init__(controller, port_id, offset) + + +class DigitalOutputPort(Port): + def __init__(self, controller: str, port_id: int, offset: float = 0): + super().__init__(controller, port_id, offset) + + +class Waveform: + def __init__(self, name: str, dictionary: Dict[str, Any]): + """A waveform played by the QOP + + :param name: name of the played WF + :type name: str + :param dictionary: parameters to initialize a WF + :type dictionary: Dict[str, Any] + """ + self.name = name + self.dict = dictionary + + +class Pulse: + def __init__(self, name: str, wfs: List[Waveform], operation: str, length: int): + """A an analog pulse + + :param name: Name for this pulse + :type name: str + :param wfs: Name of the waveform + :type wfs: List[Waveform] + :param operation: + :type operation: str + :param length: + :type length: int + """ + assert len(wfs) <= 2 + self.name = name + self.wfs = wfs + self.dict = dict() + self.dict["operation"] = operation + self.dict["length"] = length + self.dict["waveforms"] = dict() + if len(wfs) == 2: + self.dict["waveforms"]["I"] = wfs[0].name + self.dict["waveforms"]["Q"] = wfs[1].name + else: + self.dict["waveforms"]["single"] = wfs[0].name + + @property + def waveform_names(self): + return [wf.name for wf in self.wfs] + + +class Operation: + def __init__(self, pulse: Pulse, name: str = ""): + self.pulse = pulse + self.name = pulse.name if name == "" else name + + +class IntegrationWeights: + def __init__(self, name: str, cosine: List, sine: List): + """A vector of integration weights used in integration and demodulation + + :param name: name for the weights vector + :type name: str + :param cosine: a list of weights to be used with the cosine demodulation element, specified at 1 sample per nanosecond + :type cosine: List + :param sine: a list of weights to be used with the sine demodulation element, specified at 1 sample per nanosecond + :type sine: List + """ + self.name = name + self.dict = dict() + self.dict["cosine"] = cosine + self.dict["sine"] = sine + + +class Weights: + def __init__(self, weights: IntegrationWeights, name: str = ""): + self.name = weights.name if name == "" else name + self.weights = weights + + +class DigitalSample: + def __init__(self, state: int, duration: int): + """A sample to describe digital waveform + + :param state: state of the digital output either 0 or 1 + :type state: int + :param duration: duration of the output in nanoseconds + :type duration: int + """ + assert state == 0 or state == 1 + self.state = state + self.duration = duration + + +class Matrix2x2: + def __init__(self, data: List[List[float]]): + """A correction matrix for the IQ mixer + + :param data: 2x2 matrix provided as a list containing a list of rows + :type data: List[List[float]] + """ + assert len(data) == 2 + assert len(data[0]) == 2 + assert len(data[1]) == 2 + self.data = data + + +class AnalogOutputFilter: + def __init__(self, feedback: List[float], feedforward: List[float]): + """A filter applied to the analog output ports + + :param feedback: feedback taps for the output filter + :type feedback: List[float] + :param feedforward: feedforward taps for the output filter + :type feedforward: List[float] + """ + self.feedback = feedback + self.feedforward = feedforward diff --git a/tests/test_config_builder.py b/tests/test_config_builder.py new file mode 100644 index 00000000..fa5115b5 --- /dev/null +++ b/tests/test_config_builder.py @@ -0,0 +1,160 @@ +import pytest +import numpy as np + +from qualang_tools.config.configuration import * +from qualang_tools.config.components import * +from qualang_tools.config.builder import ConfigBuilder + + +@pytest.fixture +def config_resonator(): + cont = Controller("con1") + res = ReadoutResonator( + "res1", + outputs=[cont.analog_output(0), cont.analog_output(1)], + inputs=[cont.analog_input(0), cont.analog_input(1)], + intermediate_frequency=2e6, + ) + res.lo_frequency = 4e9 + wfs = [ + ArbitraryWaveform("wf1", np.linspace(0, -0.5, 16).tolist()), + ArbitraryWaveform("wf2", np.linspace(0, -0.5, 16).tolist()), + ] + + ro_pulse = MeasurePulse("ro_pulse", wfs, 16) + ro_pulse.add( + Weights(ConstantIntegrationWeights("integ_w1_I", cosine=1, sine=0, duration=16)) + ) + ro_pulse.add( + Weights( + ConstantIntegrationWeights("integ_w1_Q", cosine=0, sine=-1, duration=16) + ) + ) + ro_pulse.add( + Weights(ConstantIntegrationWeights("integ_w2_I", cosine=0, sine=1, duration=16)) + ) + ro_pulse.add( + Weights(ConstantIntegrationWeights("integ_w2_Q", cosine=1, sine=0, duration=16)) + ) + + res.add(Operation(ro_pulse)) + + cb = ConfigBuilder() + cb.add(cont) + cb.add(res) + return cb.build() + + +def test_controller(config_resonator): + config = config_resonator + assert config["version"] == 1 + assert "con1" in [*config["controllers"]] + assert config["controllers"]["con1"]["type"] == "opx1" + assert [*config["controllers"]["con1"]["analog_outputs"]] == [0, 1] + assert [*config["controllers"]["con1"]["analog_inputs"]] == [0, 1] + assert config["controllers"]["con1"]["analog_outputs"][0]["offset"] == 0 + assert config["controllers"]["con1"]["analog_outputs"][1]["offset"] == 0 + assert config["controllers"]["con1"]["analog_inputs"][0]["offset"] == 0 + assert config["controllers"]["con1"]["analog_inputs"][1]["offset"] == 0 + + +def test_element(config_resonator): + config = config_resonator + assert "res1" in [*config["elements"]] + assert config["elements"]["res1"]["time_of_flight"] == 0 + assert config["elements"]["res1"]["smearing"] == 0 + assert config["elements"]["res1"]["intermediate_frequency"] == 2e6 + assert config["elements"]["res1"]["mixInputs"]["lo_frequency"] == 4e9 + assert config["elements"]["res1"]["mixInputs"]["I"] == ("con1", 0) + assert config["elements"]["res1"]["mixInputs"]["Q"] == ("con1", 1) + assert "ro_pulse" in [*config["elements"]["res1"]["operations"]] + + +def test_pulses(config_resonator): + config = config_resonator + assert [*config["pulses"]] == ["ro_pulse"] + assert config["pulses"]["ro_pulse"]["operation"] == "measure" + assert config["pulses"]["ro_pulse"]["length"] == 16 + assert config["pulses"]["ro_pulse"]["waveforms"]["I"] == "wf1" + assert config["pulses"]["ro_pulse"]["waveforms"]["Q"] == "wf2" + assert [*config["pulses"]["ro_pulse"]["integration_weights"]] == [ + "integ_w1_I", + "integ_w1_Q", + "integ_w2_I", + "integ_w2_Q", + ] + + +def test_integration_weights(config_resonator): + config = config_resonator + assert [*config["integration_weights"]] == [ + "integ_w1_I", + "integ_w1_Q", + "integ_w2_I", + "integ_w2_Q", + ] + assert config["integration_weights"]["integ_w1_I"]["cosine"] == [(1, 16)] + assert config["integration_weights"]["integ_w1_I"]["sine"] == [(0, 16)] + assert config["integration_weights"]["integ_w1_Q"]["cosine"] == [(0, 16)] + assert config["integration_weights"]["integ_w1_Q"]["sine"] == [(-1, 16)] + assert config["integration_weights"]["integ_w2_I"]["cosine"] == [(0, 16)] + assert config["integration_weights"]["integ_w2_I"]["sine"] == [(1, 16)] + assert config["integration_weights"]["integ_w2_Q"]["cosine"] == [(1, 16)] + assert config["integration_weights"]["integ_w2_Q"]["sine"] == [(0, 16)] + + +@pytest.fixture +def config_transmon(): + + cb = ConfigBuilder() + cont = Controller("con1") + cb.add(cont) + + wf1 = ConstantWaveform("wf1", 1.0) + wf2 = ArbitraryWaveform("wf2", np.linspace(0, -0.5, 16).tolist()) + + qb1 = Transmon( + "qb1", + I=cont.analog_output(0), + Q=cont.analog_output(1), + intermediate_frequency=5e6, + ) + qb1.lo_frequency = 4e9 + qb1.add(Operation(ControlPulse("pi_pulse", [wf1, wf2], 16))) + + cb.add(qb1) + + qb2 = FluxTunableTransmon( + "qb2", + I=cont.analog_output(2), + Q=cont.analog_output(3), + fl_port=cont.analog_output(4), + intermediate_frequency=5e6, + ) + qb2.lo_frequency = 4.5e9 + qb2.add(Operation(ControlPulse("pi_pulse", [wf1, wf2], 16))) + qb2.add(Operation(ControlPulse("fl_pulse", [wf1], 16))) + + cb.add(qb2) + qb1.mixer = Mixer( + "mx1", + intermediate_frequency=5e6, + lo_frequency=4e9, + correction=Matrix2x2([[1.0, 0.0], [1.0, 0.0]]), + ) + + return cb.build() + + +def test_transmon(config_transmon): + config = config_transmon + assert [*config["elements"]] == ["qb1", "qb2", "qb2_flux_line"] + assert [*config["mixers"]] == ["mx1"] + assert [*config["waveforms"]] == ["wf1", "wf2"] + assert config["mixers"]["mx1"] == { + "intermediate_frequency": 5e6, + "lo_frequency": 4e9, + "correction": (1.0, 0.0, 1.0, 0.0), + } + assert config["elements"]["qb2_flux_line"]["singleInput"]["port"] == ("con1", 4) + assert [*config["pulses"]] == ["pi_pulse", "fl_pulse"]