예제 #1
0
def test_param_setter():

    p = Parameter("p", unit="P", get_cmd=None, set_cmd=None)
    strr = parameter_setter(p)
    assert hasattr(strr, "getter_setter_decorated")

    _strr, table = strr()
    assert _strr(0) == {"p": 0}
    assert p.get() == 0

    table.resolve_dependencies()
    assert table.nests == [["p"]]
    param_spec, = table.param_specs

    assert param_spec.name == "p"
    assert param_spec.unit == "P"
예제 #2
0
def test_param_setter_with_paramtype():

    p = Parameter("p", unit="P", get_cmd=None, set_cmd=None)
    strr = parameter_setter(p, paramtype='array')
    table = strr.parameter_table

    assert strr(0) == {"p": 0}
    assert p.get() == 0

    table.resolve_dependencies()

    assert table.nests == [["p"]]

    param_spec, = table.param_specs

    assert param_spec.name == "p"
    assert param_spec.unit == "P"
    assert param_spec.type == "array"
예제 #3
0
class Agilent_34400A(VisaInstrument):
    """
    This is the QCoDeS driver for the Agilent_34400A DMM Series,
    tested with Agilent_34401A, Agilent_34410A, and Agilent_34411A.
    """
    def __init__(self, name: str, address: str, **kwargs: Any) -> None:
        super().__init__(name, address, terminator='\n', **kwargs)

        idn = self.IDN.get()
        self.model = idn['model']

        NPLC_list = {'34401A': [0.02, 0.2, 1, 10, 100],
                     '34410A': [0.006, 0.02, 0.06, 0.2, 1, 2, 10, 100],
                     '34411A': [0.001, 0.002, 0.006, 0.02, 0.06, 0.2,
                                1, 2, 10, 100]
                     }[self.model]

        self._resolution_factor = {'34401A': [1e-4, 1e-5, 3e-6, 1e-6, 3e-7],
                                   '34410A': [6e-06, 3e-06, 1.5e-06, 7e-07,
                                              3e-07, 2e-07, 1e-07],
                                   '34411A': [3e-05, 1.5e-05, 6e-06, 3e-06,
                                              1.5e-06, 7e-07, 3e-07, 2e-07,
                                              1e-07, 3e-08]
                                   }[self.model]

        self.resolution = Parameter(
            "resolution",
            get_cmd="VOLT:DC:RES?",
            get_parser=float,
            set_cmd=self._set_resolution,
            label="Resolution",
            unit="V",
        )
        """Resolution """

        self.add_parameter('volt',
                           get_cmd='READ?',
                           label='Voltage',
                           get_parser=float,
                           unit='V')

        self.add_parameter('fetch',
                           get_cmd='FETCH?',
                           label='Voltage',
                           get_parser=float,
                           unit='V',
                           snapshot_get=False,
                           docstring=('Reads the data you asked for, i.e. '
                                      'after an `init_measurement()` you can '
                                      'read the data with fetch.\n'
                                      'Do not call this when you did not ask '
                                      'for data in the first place!'))

        self.add_parameter('NPLC',
                           get_cmd='VOLT:NPLC?',
                           get_parser=float,
                           set_cmd=self._set_nplc,
                           vals=Enum(*NPLC_list),
                           label='Integration time',
                           unit='NPLC')

        self.add_parameter('terminals',
                           get_cmd='ROUT:TERM?')

        self.add_parameter('range_auto',
                           get_cmd='VOLT:RANG:AUTO?',
                           set_cmd='VOLT:RANG:AUTO {:d}',
                           val_mapping={'on': 1,
                                        'off': 0})

        self.add_parameter('range',
                           get_cmd='SENS:VOLT:DC:RANG?',
                           get_parser=float,
                           set_cmd='SENS:VOLT:DC:RANG {:f}',
                           vals=Enum(0.1, 1.0, 10.0, 100.0, 1000.0))

        if self.model in ['34401A']:
            self.add_parameter('display_text',
                               get_cmd='DISP:TEXT?',
                               set_cmd='DISP:TEXT "{}"',
                               vals=Strings())

        elif self.model in ['34410A', '34411A']:
            self.add_parameter('display_text',
                               get_cmd='DISP:WIND1:TEXT?',
                               set_cmd='DISP:WIND1:TEXT "{}"',
                               vals=Strings())

            self.add_parameter('display_text_2',
                               get_cmd='DISP:WIND2:TEXT?',
                               set_cmd='DISP:WIND2:TEXT "{}"',
                               vals=Strings())

        self.connect_message()

    def _set_nplc(self, value: float) -> None:
        self.write(f'VOLT:NPLC {value:f}')
        # resolution settings change with NPLC
        self.resolution.get()

    def _set_resolution(self, value: float) -> None:
        rang = self.range.get()
        # convert both value*range and the resolution factors
        # to strings with few digits, so we avoid floating point
        # rounding errors.
        res_fac_strs = [f"{v * rang:.1e}" for v in self._resolution_factor]
        if f"{value:.1e}" not in res_fac_strs:
            raise ValueError(
                'Resolution setting {:.1e} ({} at range {}) '
                'does not exist. '
                'Possible values are {}'.format(value, value, rang,
                                                res_fac_strs))
        self.write(f'VOLT:DC:RES {value:.1e}')
        # NPLC settings change with resolution
        self.NPLC.get()

    def _set_range(self, value: float) -> None:
        self.write(f'SENS:VOLT:DC:RANG {value:f}')
        # resolution settings change with range
        self.resolution.get()

    def clear_errors(self) -> None:
        while True:
            err = self.ask('SYST:ERR?')
            if 'No error' in err:
                return
            print(err)

    def init_measurement(self) -> None:
        self.write('INIT')

    def display_clear(self) -> None:
        if self.model in ['34401A']:
            lines = ['WIND']
        elif self.model in ['34410A', '34411A']:
            lines = ['WIND1', 'WIND2']
        else:
            raise ValueError('unrecognized model: ' + str(self.model))

        for line in lines:
            self.write('DISP:' + line + ':TEXT:CLE')
            self.write('DISP:' + line + ':STAT 1')

    def reset(self) -> None:
        self.write('*RST')
예제 #4
0
class AMI430_3D(Instrument):
    def __init__(self, name: str, instrument_x: Union[AMI430, str],
                 instrument_y: Union[AMI430, str], instrument_z: Union[AMI430,
                                                                       str],
                 field_limit: Union[numbers.Real,
                                    Iterable[CartesianFieldLimitFunction]],
                 **kwargs: Any):
        """
        Driver for controlling three American Magnetics Model 430 magnet power
        supplies simultaneously for setting magnetic field vectors.

        The individual magnet power supplies can be passed in as either
        instances of AMI430 driver or as names of existing AMI430 instances.
        In the latter case, the instances will be found via the passed names.

        Args:
            name: a name for the instrument
            instrument_x: AMI430 instance or a names of existing AMI430
                instance for controlling the X axis of magnetic field
            instrument_y: AMI430 instance or a names of existing AMI430
                instance for controlling the Y axis of magnetic field
            instrument_z: AMI430 instance or a names of existing AMI430
                instance for controlling the Z axis of magnetic field
            field_limit: a number for maximum allows magnetic field or an
                iterable of callable field limit functions that define
                region(s) of allowed values in 3D magnetic field space
        """
        super().__init__(name, **kwargs)

        if not isinstance(name, str):
            raise ValueError("Name should be a string")

        for instrument, arg_name in zip(
            (instrument_x, instrument_y, instrument_z),
            ("instrument_x", "instrument_y", "instrument_z"),
        ):
            if not isinstance(instrument, (AMI430, str)):
                raise ValueError(
                    f"Instruments need to be instances of the class AMI430 "
                    f"or be valid names of already instantiated instances "
                    f"of AMI430 class; {arg_name} argument is "
                    f"neither of those")

        def find_ami430_with_name(ami430_name: str) -> AMI430:
            found_ami430 = AMI430.find_instrument(name=ami430_name,
                                                  instrument_class=AMI430)
            return found_ami430

        self._instrument_x = (instrument_x if isinstance(instrument_x, AMI430)
                              else find_ami430_with_name(instrument_x))
        self._instrument_y = (instrument_y if isinstance(instrument_y, AMI430)
                              else find_ami430_with_name(instrument_y))
        self._instrument_z = (instrument_z if isinstance(instrument_z, AMI430)
                              else find_ami430_with_name(instrument_z))

        self._field_limit: Union[float, Iterable[CartesianFieldLimitFunction]]
        if isinstance(field_limit, collections.abc.Iterable):
            self._field_limit = field_limit
        elif isinstance(field_limit, numbers.Real):
            # Conversion to float makes related driver logic simpler
            self._field_limit = float(field_limit)
        else:
            raise ValueError("field limit should either be a number or "
                             "an iterable of callable field limit functions.")

        self._set_point = FieldVector(x=self._instrument_x.field(),
                                      y=self._instrument_y.field(),
                                      z=self._instrument_z.field())

        # Get-only parameters that return a measured value
        self.add_parameter('cartesian_measured',
                           get_cmd=partial(self._get_measured, 'x', 'y', 'z'),
                           unit='T')

        self.add_parameter('x_measured',
                           get_cmd=partial(self._get_measured, 'x'),
                           unit='T')

        self.add_parameter('y_measured',
                           get_cmd=partial(self._get_measured, 'y'),
                           unit='T')

        self.add_parameter('z_measured',
                           get_cmd=partial(self._get_measured, 'z'),
                           unit='T')

        self.add_parameter('spherical_measured',
                           get_cmd=partial(self._get_measured, 'r', 'theta',
                                           'phi'),
                           unit='T')

        self.add_parameter('phi_measured',
                           get_cmd=partial(self._get_measured, 'phi'),
                           unit='deg')

        self.add_parameter('theta_measured',
                           get_cmd=partial(self._get_measured, 'theta'),
                           unit='deg')

        self.add_parameter('field_measured',
                           get_cmd=partial(self._get_measured, 'r'),
                           unit='T')

        self.add_parameter('cylindrical_measured',
                           get_cmd=partial(self._get_measured, 'rho', 'phi',
                                           'z'),
                           unit='T')

        self.add_parameter('rho_measured',
                           get_cmd=partial(self._get_measured, 'rho'),
                           unit='T')

        # Get and set parameters for the set points of the coordinates
        self.add_parameter('cartesian',
                           get_cmd=partial(self._get_setpoints,
                                           ('x', 'y', 'z')),
                           set_cmd=partial(self._set_setpoints,
                                           ('x', 'y', 'z')),
                           unit='T',
                           vals=Anything())

        self.add_parameter('x',
                           get_cmd=partial(self._get_setpoints, ('x', )),
                           set_cmd=partial(self._set_setpoints, ('x', )),
                           unit='T',
                           vals=Numbers())

        self.add_parameter('y',
                           get_cmd=partial(self._get_setpoints, ('y', )),
                           set_cmd=partial(self._set_setpoints, ('y', )),
                           unit='T',
                           vals=Numbers())

        self.add_parameter('z',
                           get_cmd=partial(self._get_setpoints, ('z', )),
                           set_cmd=partial(self._set_setpoints, ('z', )),
                           unit='T',
                           vals=Numbers())

        self.add_parameter('spherical',
                           get_cmd=partial(self._get_setpoints,
                                           ('r', 'theta', 'phi')),
                           set_cmd=partial(self._set_setpoints,
                                           ('r', 'theta', 'phi')),
                           unit='tuple?',
                           vals=Anything())

        self.add_parameter('phi',
                           get_cmd=partial(self._get_setpoints, ('phi', )),
                           set_cmd=partial(self._set_setpoints, ('phi', )),
                           unit='deg',
                           vals=Numbers())

        self.add_parameter('theta',
                           get_cmd=partial(self._get_setpoints, ('theta', )),
                           set_cmd=partial(self._set_setpoints, ('theta', )),
                           unit='deg',
                           vals=Numbers())

        self.add_parameter('field',
                           get_cmd=partial(self._get_setpoints, ('r', )),
                           set_cmd=partial(self._set_setpoints, ('r', )),
                           unit='T',
                           vals=Numbers())

        self.add_parameter('cylindrical',
                           get_cmd=partial(self._get_setpoints,
                                           ('rho', 'phi', 'z')),
                           set_cmd=partial(self._set_setpoints,
                                           ('rho', 'phi', 'z')),
                           unit='tuple?',
                           vals=Anything())

        self.add_parameter('rho',
                           get_cmd=partial(self._get_setpoints, ('rho', )),
                           set_cmd=partial(self._set_setpoints, ('rho', )),
                           unit='T',
                           vals=Numbers())

        self.add_parameter('block_during_ramp',
                           set_cmd=None,
                           initial_value=True,
                           unit='',
                           vals=Bool())

        self.ramp_mode = Parameter(
            name="ramp_mode",
            instrument=self,
            get_cmd=None,
            set_cmd=None,
            vals=Enum("default", "simultaneous"),
            initial_value="default",
        )

        self.ramping_state_check_interval = Parameter(
            name="ramping_state_check_interval",
            instrument=self,
            initial_value=0.05,
            unit="s",
            vals=Numbers(0, 10),
            set_cmd=None,
            get_cmd=None,
        )

        self.vector_ramp_rate = Parameter(
            name="vector_ramp_rate",
            instrument=self,
            unit="T/s",
            vals=Numbers(min_value=0.0),
            set_cmd=None,
            get_cmd=None,
            set_parser=self._set_vector_ramp_rate_units,
            docstring="Ramp rate along a line (vector) in 3D space. Only active"
            " if `ramp_mode='simultaneous'`.")
        """Ramp rate along a line (vector) in 3D field space"""

    def _set_vector_ramp_rate_units(self, val: float) -> float:
        _, common_ramp_rate_units = self._raise_if_not_same_field_and_ramp_rate_units(
        )
        self.vector_ramp_rate.unit = common_ramp_rate_units
        return val

    def ramp_simultaneously(self, setpoint: FieldVector,
                            duration: float) -> None:
        """
        Ramp all axes simultaneously to the given setpoint and in the given time

        The method calculates and sets the required ramp rates per magnet
        axis, and then initiates a ramp simultaneously on all the axes. The
        trajectory of the tip of the magnetic field vector is thus linear in
        3D space, from the current field value to the setpoint.

        If ``block_during_ramp`` parameter is ``True``, the method will block
        until all axes finished ramping.

        It is required for all axis instruments to have the same units for
        ramp rate and field, otherwise an exception is raised. The given
        setpoint and time are assumed to be in those common units.

        Args:
            setpoint: ``FieldVector`` setpoint
            duration: time in which the setpoint field has to be reached on all axes

        """
        (
            common_field_units,
            common_ramp_rate_units,
        ) = self._raise_if_not_same_field_and_ramp_rate_units()

        self.log.debug(
            f"Simultaneous ramp: setpoint {setpoint.repr_cartesian()} "
            f"{common_field_units} in {duration} {common_ramp_rate_units}")

        # Get starting field value

        start_field = self._get_measured_field_vector()
        self.log.debug(
            f"Simultaneous ramp: start {start_field.repr_cartesian()} "
            f"{common_field_units}")
        self.log.debug(
            f"Simultaneous ramp: delta {(setpoint - start_field).repr_cartesian()} "
            f"{common_field_units}")

        # Calculate new vector ramp rate based on time and setpoint

        vector_ramp_rate = self.calculate_vector_ramp_rate_from_duration(
            start=start_field, setpoint=setpoint, duration=duration)
        self.vector_ramp_rate(vector_ramp_rate)
        self.log.debug(
            f"Simultaneous ramp: new vector ramp rate for {self.full_name} "
            f"is {vector_ramp_rate} {common_ramp_rate_units}")

        # Launch the simultaneous ramp

        self.ramp_mode("simultaneous")
        self.cartesian(setpoint.get_components("x", "y", "z"))

    @staticmethod
    def calculate_axes_ramp_rates_for(
            start: FieldVector, setpoint: FieldVector,
            duration: float) -> Tuple[float, float, float]:
        """
        Given starting and setpoint fields and expected ramp time calculates
        required ramp rates for x, y, z axes (in this order) where axes are
        ramped simultaneously.
        """
        vector_ramp_rate = AMI430_3D.calculate_vector_ramp_rate_from_duration(
            start, setpoint, duration)
        return AMI430_3D.calculate_axes_ramp_rates_from_vector_ramp_rate(
            start, setpoint, vector_ramp_rate)

    @staticmethod
    def calculate_vector_ramp_rate_from_duration(start: FieldVector,
                                                 setpoint: FieldVector,
                                                 duration: float) -> float:
        return setpoint.distance(start) / duration

    @staticmethod
    def calculate_axes_ramp_rates_from_vector_ramp_rate(
            start: FieldVector, setpoint: FieldVector,
            vector_ramp_rate: float) -> Tuple[float, float, float]:
        delta_field = setpoint - start
        ramp_rate_3d = delta_field / delta_field.norm() * vector_ramp_rate
        return abs(ramp_rate_3d["x"]), abs(ramp_rate_3d["y"]), abs(
            ramp_rate_3d["z"])

    def _raise_if_not_same_field_and_ramp_rate_units(self) -> Tuple[str, str]:
        instruments = (self._instrument_x, self._instrument_y,
                       self._instrument_z)

        field_units_of_instruments = defaultdict(set)
        ramp_rate_units_of_instruments = defaultdict(set)

        for instrument in instruments:
            ramp_rate_units_of_instruments[
                instrument.ramp_rate_units.cache.get()].add(
                    instrument.full_name)
            field_units_of_instruments[instrument.field_units.cache.get()].add(
                instrument.full_name)

        if len(field_units_of_instruments) != 1:
            raise ValueError(f"Magnet axes instruments should have the same "
                             f"`field_units`, instead they have: "
                             f"{field_units_of_instruments}")

        if len(ramp_rate_units_of_instruments) != 1:
            raise ValueError(f"Magnet axes instruments should have the same "
                             f"`ramp_rate_units`, instead they have: "
                             f"{ramp_rate_units_of_instruments}")

        common_field_units = tuple(field_units_of_instruments.keys())[0]
        common_ramp_rate_units = tuple(
            ramp_rate_units_of_instruments.keys())[0]

        return common_field_units, common_ramp_rate_units

    def _verify_safe_setpoint(
            self, setpoint_values: Tuple[float, float, float]) -> bool:
        if isinstance(self._field_limit, (int, float)):
            return np.linalg.norm(setpoint_values) < self._field_limit

        answer = any([
            limit_function(*setpoint_values)
            for limit_function in self._field_limit
        ])

        return answer

    def _adjust_child_instruments(self, values: Tuple[float, float,
                                                      float]) -> None:
        """
        Set the fields of the x/y/z magnets. This function is called
        whenever the field is changed and performs several safety checks
        to make sure no limits are exceeded.

        Args:
            values: a tuple of cartesian coordinates (x, y, z).
        """
        self.log.debug("Checking whether fields can be set")

        # Check if exceeding the global field limit
        if not self._verify_safe_setpoint(values):
            raise ValueError("_set_fields aborted; field would exceed limit")

        # Check if the individual instruments are ready
        for name, value in zip(["x", "y", "z"], values):

            instrument = getattr(self, f"_instrument_{name}")
            if instrument.ramping_state() == "ramping":
                msg = '_set_fields aborted; magnet {} is already ramping'
                raise AMI430Exception(msg.format(instrument))

        # Now that we know we can proceed, call the individual instruments

        self.log.debug("Field values OK, proceeding")

        if self.ramp_mode() == "simultaneous":
            self._perform_simultaneous_ramp(values)
        else:
            self._perform_default_ramp(values)

    def _update_individual_axes_ramp_rates(
            self, values: Tuple[float, float, float]) -> None:
        if self.vector_ramp_rate() is None or self.vector_ramp_rate() == 0:
            raise ValueError(
                'The value of the `vector_ramp_rate` Parameter is '
                'currently None or 0. Set it to an appropriate '
                'value to use the simultaneous ramping feature.')

        new_axes_ramp_rates = self.calculate_axes_ramp_rates_from_vector_ramp_rate(
            start=self._get_measured_field_vector(),
            setpoint=FieldVector(x=values[0], y=values[1], z=values[2]),
            vector_ramp_rate=self.vector_ramp_rate.get(),
        )
        instruments = (self._instrument_x, self._instrument_y,
                       self._instrument_z)
        for instrument, new_axis_ramp_rate in zip(instruments,
                                                  new_axes_ramp_rates):
            instrument.ramp_rate.set(new_axis_ramp_rate)
            self.log.debug(
                f"Simultaneous ramp: new rate for {instrument.full_name} "
                f"is {new_axis_ramp_rate} {instrument.ramp_rate.unit}")

    def _perform_simultaneous_ramp(self, values: Tuple[float, float,
                                                       float]) -> None:
        self._update_individual_axes_ramp_rates(values)

        axes = (self._instrument_x, self._instrument_y, self._instrument_z)

        for axis_instrument, value in zip(axes, values):
            current_actual = axis_instrument.field()

            # If the new set point is practically equal to the
            # current one then do nothing
            if np.isclose(value, current_actual, rtol=0, atol=1e-8):
                self.log.debug(
                    f"Simultaneous ramp: {axis_instrument.short_name} is "
                    f"already at target field {value} "
                    f"{axis_instrument.field.unit} "
                    f"({current_actual} exactly)")
                continue

            self.log.debug(
                f"Simultaneous ramp: setting {axis_instrument.short_name} "
                f"target field to {value} {axis_instrument.field.unit}")
            axis_instrument.set_field(value,
                                      perform_safety_check=False,
                                      block=False)

        if self.block_during_ramp() is True:
            self.log.debug(
                f"Simultaneous ramp: blocking until ramp is finished")
            self.wait_while_all_axes_ramping()

        self.log.debug(f"Simultaneous ramp: returning from the ramp call")

    def _perform_default_ramp(self, values: Tuple[float, float,
                                                  float]) -> None:
        operators: Tuple[Callable[[Any, Any], bool],
                         ...] = (np.less, np.greater)
        for operator in operators:
            # First ramp the coils that are decreasing in field strength.
            # This will ensure that we are always in a safe region as
            # far as the quenching of the magnets is concerned
            for name, value in zip(["x", "y", "z"], values):

                instrument = getattr(self, f"_instrument_{name}")
                current_actual = instrument.field()

                # If the new set point is practically equal to the
                # current one then do nothing
                if np.isclose(value, current_actual, rtol=0, atol=1e-8):
                    continue
                # evaluate if the new set point is smaller or larger
                # than the current value
                if not operator(abs(value), abs(current_actual)):
                    continue

                instrument.set_field(value,
                                     perform_safety_check=False,
                                     block=self.block_during_ramp.get())

    def wait_while_all_axes_ramping(self) -> None:
        """ Wait and blocks as long as any magnet axis is ramping. """
        while self.any_axis_is_ramping():
            self._instrument_x._sleep(self.ramping_state_check_interval.get())

    def any_axis_is_ramping(self) -> bool:
        """
        Returns True if any of the magnet axes are currently ramping, or False
        if none of the axes are ramping.
        """
        return any(axis_instrument.ramping_state() == "ramping"
                   for axis_instrument in (
                       self._instrument_x,
                       self._instrument_y,
                       self._instrument_z,
                   ))

    def pause(self) -> None:
        """ Pause all magnet axes. """
        for axis_instrument in (self._instrument_x, self._instrument_y,
                                self._instrument_z):
            axis_instrument.pause()

    def _request_field_change(self, instrument: AMI430,
                              value: numbers.Real) -> None:
        """
        This method is called by the child x/y/z magnets if they are set
        individually. It results in additional safety checks being
        performed by this 3D driver.
        """
        if instrument is self._instrument_x:
            self._set_x(value)
        elif instrument is self._instrument_y:
            self._set_y(value)
        elif instrument is self._instrument_z:
            self._set_z(value)
        else:
            msg = 'This magnet doesnt belong to its specified parent {}'
            raise NameError(msg.format(self))

    def _get_measured_field_vector(self) -> FieldVector:
        return FieldVector(
            x=self._instrument_x.field(),
            y=self._instrument_y.field(),
            z=self._instrument_z.field(),
        )

    def _get_measured(self,
                      *names: str) -> Union[numbers.Real, List[numbers.Real]]:
        measured_field_vector = self._get_measured_field_vector()

        measured_values = measured_field_vector.get_components(*names)

        # Convert angles from radians to degrees
        d = dict(zip(names, measured_values))

        # Do not do "return list(d.values())", because then there is
        # no guaranty that the order in which the values are returned
        # is the same as the original intention
        return_value = [d[name] for name in names]

        if len(names) == 1:
            return_value = return_value[0]

        return return_value

    def _get_setpoints(
            self,
            names: Sequence[str]) -> Union[numbers.Real, List[numbers.Real]]:

        measured_values = self._set_point.get_components(*names)

        # Convert angles from radians to degrees
        d = dict(zip(names, measured_values))
        return_value = [d[name] for name in names]
        # Do not do "return list(d.values())", because then there is
        # no guarantee that the order in which the values are returned
        # is the same as the original intention

        if len(names) == 1:
            return_value = return_value[0]

        return return_value

    def _set_setpoints(self, names: Sequence[str],
                       values: Sequence[float]) -> None:

        kwargs = dict(zip(names, np.atleast_1d(values)))

        set_point = FieldVector()
        set_point.copy(self._set_point)
        if len(kwargs) == 3:
            set_point.set_vector(**kwargs)
        else:
            set_point.set_component(**kwargs)

        self._adjust_child_instruments(set_point.get_components("x", "y", "z"))

        self._set_point = set_point
예제 #5
0
class SGS100AInterface(InstrumentInterface):
    def __init__(self, instrument_name, **kwargs):
        super().__init__(instrument_name, **kwargs)

        self._input_channels = {
            "I": Channel(instrument_name=self.instrument_name(), name="I", input=True),
            "Q": Channel(instrument_name=self.instrument_name(), name="Q", input=True),
            "pulse_mod": Channel(
                instrument_name=self.instrument_name(), name="pulse_mod", input=True
            ),
        }

        self._output_channels = {
            "RF_out": Channel(
                instrument_name=self.instrument_name(), name="RF_out", output=True
            )
        }

        self._channels = {
            **self._input_channels,
            **self._output_channels,
            "sync": Channel(
                instrument_name=self.instrument_name(), name="sync", output=True
            ),
        }

        self.pulse_implementations = [
            SinePulseImplementation(
                pulse_requirements=[
                    ("frequency", {"min": 1e6, "max": 40e9}),
                    ("power", {"min": -120, "max": 25}),
                    ("duration", {"min": 100e-9}),
                ]
            ),
            FrequencyRampPulseImplementation(
                pulse_requirements=[
                    ("frequency_start", {"min": 1e6, "max": 40e9}),
                    ("frequency_stop", {"min": 1e6, "max": 40e9}),
                    ("power", {"min": -120, "max": 25}),
                    ("duration", {"min": 100e-9}),
                ]
            ),
        ]

        self.envelope_padding = Parameter(
            "envelope_padding",
            unit="s",
            set_cmd=None,
            initial_value=0,
            vals=vals.Numbers(min_value=0, max_value=10e-3),
            docstring="Padding for any pulses that use IQ modulation. "
            "This is to ensure that any IQ pulses such as sine waves "
            "are applied a bit before the pulse starts. The marker pulse "
            "used for pulse modulation does not use any envelope padding.",
        )
        self.marker_amplitude = Parameter(
            unit="V",
            set_cmd=None,
            initial_value=1.5,
            docstring="Amplitude of marker pulse used for gating",
        )
        self.fix_frequency = Parameter(
            set_cmd=None,
            initial_value=False,
            vals=vals.Bool(),
            docstring="Whether to fix frequency to current value, or to "
            "dynamically choose frequency during setup",
        )
        self.frequency_carrier_choice = Parameter(
            set_cmd=None,
            initial_value="center",
            vals=vals.MultiType(vals.Enum("min", "center", "max"), vals.Numbers()),
            docstring="The choice for the microwave frequency, This is used if "
            "pulses with multiple frequencies are used, or if frequency "
            "modulation is needed. Ignored if fix_frequency = True. "
            'Can be either "max", "min", or "center", or a '
            "number which is the offset from the center",
        )
        self.frequency = Parameter(
            unit="Hz", set_cmd=None, initial_value=self.instrument.frequency()
        )
        self.power = Parameter(
            unit="dBm",
            set_cmd=None,
            initial_value=self.instrument.power(),
            docstring="Power that the microwave source will be set to. "
            "Set to equal the maximum power of the pulses",
        )
        self.IQ_modulation = Parameter(
            initial_value=None,
            vals=vals.Bool(),
            docstring="Whether to use IQ modulation. Cannot be directly set, but "
            "is set internally if there are pulses with multiple "
            "frequencies, self.fix_frequency() is True, or "
            "self.force_IQ_modulation() is True.",
        )
        self.IQ_channels = Parameter(
            initial_value='IQ',
            vals=vals.Enum('IQ', 'I', 'Q'),
            set_cmd=None,
            docstring="Which channels to use for IQ modulation."
                      "Double-sideband modulation is used if only 'I' or 'Q' "
                      "is chosen, while single-sideband modulation is used when"
                      "'IQ' is chosen."
        )
        self.force_IQ_modulation = Parameter(
            initial_value=False,
            vals=vals.Bool(),
            set_cmd=None,
            docstring="Whether to force IQ modulation.",
        )

        self.marker_per_pulse = Parameter(
            initial_value=True,
            vals=vals.Bool(),
            set_cmd=None,
            docstring='Use a separate marker per pulse. If False, a single '
                      'marker pulse is requested for the first pulse to the last '
                      'pulse. In this case, envelope padding will be added to '
                      'either side of the single marker pulse.'
        )

        # Add parameters that are not set via setup
        self.additional_settings = ParameterNode()
        for parameter_name in [
            "phase",
            "maximum_power",
            "IQ_impairment",
            "I_leakage",
            "Q_leakage",
            "Q_offset",
            "IQ_ratio",
            "IQ_wideband",
            "IQ_crestfactor",
            "reference_oscillator_source",
            "reference_oscillator_output_frequency",
            "reference_oscillator_external_frequency",
        ]:
            parameter = getattr(self.instrument, parameter_name)
            setattr(self.additional_settings, parameter_name, parameter)

    def determine_instrument_settings(self, update: bool = False) -> dict:
        """Determine the frequency settings from parameters and  pulse sequence

        Used to determine additional pulses and during setup

        Args:
            update: Update the interface parameters

        Returns:
            Dictionary with following items:
            - ``IQ_modulation``: Use IQ modulation
            - ``IQ_channels``: IQ channels to use. Can be 'I', 'Q', 'IQ'
            - ``frequency``: carrier frequency
            - ``power`: output power
            - ``marker_per_pulse``: Create marker pulse for each pulse.
              If False, a single marker pulse is created spanning all pulses.
              Reverted to True with warning if IQ_modulation is False
        """
        settings = {}

        assert all(pulse.frequency_sideband is None for pulse in self.pulse_sequence)

        # Determine minimum and maximum frequency
        min_frequency = max_frequency = None
        phases = []
        for pulse in self.pulse_sequence:
            pulse_min_frequency = pulse_max_frequency = pulse.frequency
            if getattr(pulse, "frequency_deviation", None) is not None:
                pulse_min_frequency -= pulse.frequency_deviation
                pulse_max_frequency += pulse.frequency_deviation

            if min_frequency is None or pulse_min_frequency < min_frequency:
                min_frequency = pulse_min_frequency
            if max_frequency is None or pulse_max_frequency > max_frequency:
                max_frequency = pulse_max_frequency
            phases.append(int(round(pulse.phase)))
        min_frequency = int(round(min_frequency))
        max_frequency = int(round(max_frequency))


        # Check whether to use IQ modulation
        if (
            min_frequency != max_frequency
            or self.fix_frequency()
            or self.force_IQ_modulation()
            or len(set(phases)) > 1
        ):
            # Set protected IQ_modulation parameter
            settings["IQ_modulation"] = True

            if not self.fix_frequency():
                if self.frequency_carrier_choice() == "center":
                    settings["frequency"] = (min_frequency + max_frequency) / 2
                elif self.frequency_carrier_choice() == "min":
                    settings["frequency"] = min_frequency
                elif self.frequency_carrier_choice() == "max":
                    settings["frequency"] = max_frequency
                else:
                    settings["frequency"] = (min_frequency + max_frequency) / 2
                    settings["frequency"] += self.frequency_carrier_choice()
            else:
                settings["frequency"] = self.frequency()
        else:
            # Set protected IQ_modulation parameter
            settings["IQ_modulation"] = False
            settings["frequency"] = min_frequency

        settings['marker_per_pulse'] = self.marker_per_pulse()
        if not settings['marker_per_pulse'] and not settings['IQ_modulation']:
            logger.warning("Must use marker_per_pulse if IQ_modulation is off")
            settings['marker_per_pulse'] = True

        # If IQ modulation is used, ensure pulses are spaced by more than twice
        # the envelope padding
        if settings["IQ_modulation"]:
            # Set microwave power to the maximum power of all the pulses.
            # Pulses with lower power will have less IQ modulation amplitude
            settings["power"] = max(pulse.power for pulse in self.pulse_sequence)

            if settings['marker_per_pulse']:
                # Perform an efficient check of spacing between pulses
                t_start_list = self.pulse_sequence.t_start_list
                t_stop_list = self.pulse_sequence.t_stop_list

                t_start_2D = np.tile(t_start_list, (len(t_stop_list), 1))
                t_stop_2D = np.tile(t_start_list, (len(t_start_list), 1)).transpose()
                t_difference_2D = t_start_2D - t_stop_2D

                overlap_elems = t_difference_2D > 0
                overlap_elems &= t_difference_2D < 2 * self.envelope_padding()

                if np.any(overlap_elems):
                    overlapping_pulses = [
                        (pulse1, pulse2)
                        for pulse1 in self.pulse_sequence
                        for pulse2 in self.pulse_sequence
                        if 0 <= pulse1.t_start - pulse2.t_stop < 2 * self.envelope_padding()
                    ]
                    raise RuntimeError(
                        f"Spacing between successive microwave pulses is less than "
                        f"2*envelope_padding: {overlapping_pulses}"
                    )
        else:
            powers = {pulse.power for pulse in self.pulse_sequence}
            if len(powers) > 1:
                raise RuntimeError(
                    "Without IQ modulation, microwave pulses cannot have "
                    "different powers."
                )
            settings["power"] = next(iter(powers))

        settings["IQ_channels"] = self.IQ_channels()

        if update:
            self.frequency = settings["frequency"]
            self.power = settings["power"]
            self.IQ_modulation._latest["raw_value"] = settings["IQ_modulation"]
            self.IQ_modulation.get()

        return settings

    def get_additional_pulses(self, connections) -> List[Pulse]:
        """Additional pulses needed by instrument after targeting of main pulses

        Args:
            connections: List of all connections in the layout

        Returns:
            List of additional pulses, such as IQ modulation pulses
        """
        if not self.pulse_sequence:
            return []

        settings = self.determine_instrument_settings()

        additional_pulses = []

        # Handle marker pulses first
        if settings['marker_per_pulse']:
            # Add a marker pulse per pulse
            marker_pulse = None
            for pulse in self.pulse_sequence:
                if marker_pulse is not None and pulse.t_start == marker_pulse.t_stop:
                    # Marker pulse already exists, extend the duration
                    marker_pulse.t_stop = pulse.t_stop
                else:
                    # Request a new marker pulse
                    marker_pulse = MarkerPulse(
                        t_start=pulse.t_start,
                        t_stop=pulse.t_stop,
                        amplitude=self.marker_amplitude(),
                        connection_requirements={
                            "input_instrument": self.instrument_name(),
                            "input_channel": "pulse_mod",
                        },
                    )
                    additional_pulses.append(marker_pulse)
        else:
            # Add single marker pulse before first pulse to after last pulse
            t_start = min(self.pulse_sequence.t_start_list) - self.envelope_padding()
            t_stop = max(self.pulse_sequence.t_stop_list) + self.envelope_padding()

            additional_pulses.append(
                MarkerPulse(
                    t_start=t_start,
                    t_stop=t_stop,
                    amplitude=self.marker_amplitude(),
                    connection_requirements={
                        "input_instrument": self.instrument_name(),
                        "input_channel": "pulse_mod",
                    },
                ))

        # Now add additional pulses requested by each pulse in pulse sequence
        for pulse in self.pulse_sequence:
            # Handle any additional pulses such as those for IQ modulation
            additional_pulses += pulse.implementation.get_additional_pulses(
                self, **settings
            )

        return additional_pulses

    def setup(self, **kwargs):
        """Setup all instrument settings to output pulse sequence.
        Parameters that are not automatically set are in interface.additional_settings
        """
        self.stop()

        # Update frequency, IQ_modulation, and power
        self.determine_instrument_settings(update=True)

        # Use normal operation mode, not baseband bypass
        self.instrument.operation_mode("normal")

        self.instrument.frequency(self.frequency())
        self.instrument.power(self.power())

        self.instrument.pulse_modulation_state("on")
        self.instrument.pulse_modulation_source("external")

        if self.IQ_modulation():
            self.instrument.IQ_modulation("on")
        else:
            self.instrument.IQ_modulation("off")

        # targeted_pulse_sequence is the pulse sequence that is currently setup
        self.targeted_pulse_sequence = self.pulse_sequence
        self.targeted_input_pulse_sequence = self.input_pulse_sequence

    def start(self):
        """Turn all active instrument channels on"""
        self.instrument.on()

    def stop(self):
        self.instrument.off()