Пример #1
0
    def _set_setpoints(self, names, values):

        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
Пример #2
0
    def _set_target(self, coordinate: str, target: float) -> None:
        """
        The function to set a target value for a coordinate, i.e. the set_cmd
        for the XXX_target parameters
        """
        # first validate the new target
        valid_vec = FieldVector()
        valid_vec.copy(self._target_vector)
        valid_vec.set_component(**{coordinate: target})
        components = valid_vec.get_components('x', 'y', 'z')
        if not self._field_limits(*components):
            raise ValueError(f'Cannot set {coordinate} target to {target}, '
                             'that would violate the field_limits. ')

        # update our internal target cache
        self._target_vector.set_component(**{coordinate: target})

        # actually assign the target on the slaves
        cartesian_targ = self._target_vector.get_components('x', 'y', 'z')
        for targ, slave in zip(cartesian_targ, self.submodules.values()):
            slave.field_target(targ)
Пример #3
0
class AMI430_3D(Instrument):
    def __init__(self, name, instrument_x, instrument_y, instrument_z,
                 field_limit, **kwargs):
        super().__init__(name, **kwargs)

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

        instruments = [instrument_x, instrument_y, instrument_z]

        if not all(
            [isinstance(instrument, AMI430) for instrument in instruments]):
            raise ValueError("Instruments need to be instances "
                             "of the class AMI430")

        self._instrument_x = instrument_x
        self._instrument_y = instrument_y
        self._instrument_z = instrument_z

        if repr(field_limit).isnumeric() or isinstance(field_limit,
                                                       collections.Iterable):
            self._field_limit = field_limit
        else:
            raise ValueError("field limit should either be"
                             " a number or an iterable")

        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=self._set_cartesian,
                           unit='T',
                           vals=Anything())

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

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

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

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

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

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

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

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

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

    def _verify_safe_setpoint(self, setpoint_values):

        if repr(self._field_limit).isnumeric():
            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 _set_fields(self, values):
        """
        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 (tuple): a tuple of cartesian coordinates (x, y, z).
        """
        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, "_instrument_{}".format(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

        log.debug("Field values OK, proceeding")
        for operator in [np.less, np.greater]:
            # 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, "_instrument_{}".format(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)

    def _request_field_change(self, instrument, value):
        """
        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(self, *names):

        x = self._instrument_x.field()
        y = self._instrument_y.field()
        z = self._instrument_z.field()
        measured_values = FieldVector(x=x, y=y, z=z).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):

        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_cartesian(self, values):
        x, y, z = values
        self._set_point.set_vector(x=x, y=y, z=z)
        self._set_fields(self._set_point.get_components("x", "y", "z"))

    def _set_x(self, x):
        self._set_point.set_component(x=x)
        self._set_fields(self._set_point.get_components("x", "y", "z"))

    def _set_y(self, y):
        self._set_point.set_component(y=y)
        self._set_fields(self._set_point.get_components("x", "y", "z"))

    def _set_z(self, z):
        self._set_point.set_component(z=z)
        self._set_fields(self._set_point.get_components("x", "y", "z"))

    def _set_spherical(self, values):
        r, theta, phi = values
        self._set_point.set_vector(r=r, theta=theta, phi=phi)
        self._set_fields(self._set_point.get_components("x", "y", "z"))

    def _set_r(self, r):
        self._set_point.set_component(r=r)
        self._set_fields(self._set_point.get_components("x", "y", "z"))

    def _set_theta(self, theta):
        self._set_point.set_component(theta=theta)
        self._set_fields(self._set_point.get_components("x", "y", "z"))

    def _set_phi(self, phi):
        self._set_point.set_component(phi=phi)
        self._set_fields(self._set_point.get_components("x", "y", "z"))

    def _set_cylindrical(self, values):
        rho, phi, z = values
        self._set_point.set_vector(rho=rho, phi=phi, z=z)
        self._set_fields(self._set_point.get_components("x", "y", "z"))

    def _set_rho(self, rho):
        self._set_point.set_component(rho=rho)
        self._set_fields(self._set_point.get_components("x", "y", "z"))
Пример #4
0
class MercuryiPS(VisaInstrument):
    """
    Driver class for the QCoDeS Oxford Instruments MercuryiPS magnet power
    supply
    """
    def __init__(self,
                 name: str,
                 address: str,
                 visalib=None,
                 field_limits: Optional[Callable[[float, float, float],
                                                 bool]] = None,
                 **kwargs) -> None:
        """
        Args:
            name: The name to give this instrument internally in QCoDeS
            address: The VISA resource of the instrument. Note that a
                socket connection to port 7020 must be made
            visalib: The VISA library to use. Leave blank if not in simulation
                mode.
            field_limits: A function describing the allowed field
                range (T). The function shall take (x, y, z) as an input and
                return a boolean describing whether that field value is
                acceptable.
        """

        if field_limits is not None and not (callable(field_limits)):
            raise ValueError('Got wrong type of field_limits. Must be a '
                             'function from (x, y, z) -> Bool. Received '
                             f'{type(field_limits)} instead.')

        if visalib:
            visabackend = visalib.split('@')[1]
        else:
            visabackend = 'NI'

        # ensure that a socket is used unless we are in simulation mode
        if not address.endswith('SOCKET') and visabackend != 'sim':
            raise ValueError('Incorrect VISA resource name. Must be of type '
                             'TCPIP0::XXX.XXX.XXX.XXX::7020::SOCKET.')

        super().__init__(name,
                         address,
                         terminator='\n',
                         visalib=visalib,
                         **kwargs)

        # to ensure a correct snapshot, we must wrap the get function
        self.IDN.get = self.IDN._wrap_get(self._idn_getter)

        self.firmware = self.IDN()['firmware']

        # TODO: Query instrument to ensure which PSUs are actually present
        for grp in ['GRPX', 'GRPY', 'GRPZ']:
            psu_name = grp
            psu = MercurySlavePS(self, psu_name, grp)
            self.add_submodule(psu_name, psu)

        self._field_limits = (field_limits
                              if field_limits else lambda x, y, z: True)

        self._target_vector = FieldVector(x=self.GRPX.field(),
                                          y=self.GRPY.field(),
                                          z=self.GRPZ.field())

        for coord, unit in zip(
            ['x', 'y', 'z', 'r', 'theta', 'phi', 'rho'],
            ['T', 'T', 'T', 'T', 'degrees', 'degrees', 'T']):
            self.add_parameter(name=f'{coord}_target',
                               label=f'{coord.upper()} target field',
                               unit=unit,
                               get_cmd=partial(self._get_component, coord),
                               set_cmd=partial(self._set_target, coord))

            self.add_parameter(name=f'{coord}_measured',
                               label=f'{coord.upper()} measured field',
                               unit=unit,
                               get_cmd=partial(self._get_measured, [coord]))

            self.add_parameter(name=f'{coord}_ramp',
                               label=f'{coord.upper()} ramp field',
                               unit=unit,
                               docstring='A safe ramp for each coordinate',
                               get_cmd=partial(self._get_component, coord),
                               set_cmd=partial(self._set_target_and_ramp,
                                               coord, 'safe'))

            if coord in ['r', 'theta', 'phi', 'rho']:
                self.add_parameter(
                    name=f'{coord}_simulramp',
                    label=f'{coord.upper()} ramp field',
                    unit=unit,
                    docstring='A simultaneous blocking ramp for a '
                    'combined coordinate',
                    get_cmd=partial(self._get_component, coord),
                    set_cmd=partial(self._set_target_and_ramp, coord,
                                    'simul_block'))

        # FieldVector-valued parameters #

        self.add_parameter(name="field_target",
                           label="target field",
                           unit="T",
                           get_cmd=self._get_target_field,
                           set_cmd=self._set_target_field)

        self.add_parameter(name="field_measured",
                           label="measured field",
                           unit="T",
                           get_cmd=self._get_field)

        self.add_parameter(name="field_ramp_rate",
                           label="ramp rate",
                           unit="T/s",
                           get_cmd=self._get_ramp_rate,
                           set_cmd=self._set_ramp_rate)

        self.connect_message()

    def _get_component(self, coordinate: str) -> float:
        return self._target_vector.get_components(coordinate)[0]

    def _get_target_field(self) -> FieldVector:
        return FieldVector(
            **{coord: self._get_component(coord)
               for coord in 'xyz'})

    def _get_ramp_rate(self) -> FieldVector:
        return FieldVector(
            x=self.GRPX.field_ramp_rate(),
            y=self.GRPY.field_ramp_rate(),
            z=self.GRPZ.field_ramp_rate(),
        )

    def _set_ramp_rate(self, rate: FieldVector) -> None:
        self.GRPX.field_ramp_rate(rate.x)
        self.GRPY.field_ramp_rate(rate.y)
        self.GRPZ.field_ramp_rate(rate.z)

    def _get_measured(self,
                      coordinates: List[str]) -> Union[float, List[float]]:
        """
        Get the measured value of a coordinate. Measures all three fields
        and computes whatever coordinate we asked for.
        """
        meas_field = FieldVector(x=self.GRPX.field(),
                                 y=self.GRPY.field(),
                                 z=self.GRPZ.field())

        if len(coordinates) == 1:
            return meas_field.get_components(*coordinates)[0]
        else:
            return meas_field.get_components(*coordinates)

    def _get_field(self) -> FieldVector:
        return FieldVector(x=self.x_measured(),
                           y=self.y_measured(),
                           z=self.z_measured())

    def _set_target(self, coordinate: str, target: float) -> None:
        """
        The function to set a target value for a coordinate, i.e. the set_cmd
        for the XXX_target parameters
        """
        # first validate the new target
        valid_vec = FieldVector()
        valid_vec.copy(self._target_vector)
        valid_vec.set_component(**{coordinate: target})
        components = valid_vec.get_components('x', 'y', 'z')
        if not self._field_limits(*components):
            raise ValueError(f'Cannot set {coordinate} target to {target}, '
                             'that would violate the field_limits. ')

        # update our internal target cache
        self._target_vector.set_component(**{coordinate: target})

        # actually assign the target on the slaves
        cartesian_targ = self._target_vector.get_components('x', 'y', 'z')
        for targ, slave in zip(cartesian_targ, self.submodules.values()):
            slave.field_target(targ)

    def _set_target_field(self, field: FieldVector) -> None:
        for coord in 'xyz':
            self._set_target(coord, field[coord])

    def _idn_getter(self) -> Dict[str, str]:
        """
        Parse the raw non-SCPI compliant IDN string into an IDN dict

        Returns:
            The normal IDN dict
        """
        raw_idn_string = self.ask('*IDN?')
        resps = raw_idn_string.split(':')

        idn_dict = {
            'model': resps[2],
            'vendor': resps[1],
            'serial': resps[3],
            'firmware': resps[4]
        }

        return idn_dict

    def _ramp_simultaneously(self) -> None:
        """
        Ramp all three fields to their target simultaneously at their given
        ramp rates. NOTE: there is NO guarantee that this does not take you
        out of your safe region. Use with care.
        """
        for slave in self.submodules.values():
            slave.ramp_to_target()

    def _ramp_simultaneously_blocking(self) -> None:
        """
        Ramp all three fields to their target simultaneously at their given
        ramp rates. NOTE: there is NO guarantee that this does not take you
        out of your safe region. Use with care. This function is BLOCKING.
        """
        self._ramp_simultaneously()

        for slave in self.submodules.values():
            # wait for the ramp to finish, we don't care about the order
            while slave.ramp_status() == 'TO SET':
                time.sleep(0.1)

        self.update_field()

    def _ramp_safely(self) -> None:
        """
        Ramp all three fields to their target using the 'first-down-then-up'
        sequential ramping procedure. This function is BLOCKING.
        """
        meas_vals = self._get_measured(['x', 'y', 'z'])
        targ_vals = self._target_vector.get_components('x', 'y', 'z')
        order = np.argsort(np.abs(np.array(targ_vals) - np.array(meas_vals)))

        for slave in np.array(list(self.submodules.values()))[order]:
            slave.ramp_to_target()
            # now just wait for the ramp to finish
            # (unless we are testing)
            if self.visabackend == 'sim':
                pass
            else:
                while slave.ramp_status() == 'TO SET':
                    time.sleep(0.1)

        self.update_field()

    def update_field(self) -> None:
        """
        Update all the field components.
        """
        coords = ['x', 'y', 'z', 'r', 'theta', 'phi', 'rho']
        meas_field = self._get_field()
        [getattr(self, f'{i}_measured').get() for i in coords]

    def is_ramping(self) -> bool:
        """
        Returns True if any axis has a ramp status that is either 'TO SET' or
        'TO ZERO'
        """
        ramping_statuus = ['TO SET', 'TO ZERO']
        is_x_ramping = self.GRPX.ramp_status() in ramping_statuus
        is_y_ramping = self.GRPY.ramp_status() in ramping_statuus
        is_z_ramping = self.GRPZ.ramp_status() in ramping_statuus

        return is_x_ramping or is_y_ramping or is_z_ramping

    def set_new_field_limits(self, limit_func: Callable) -> None:
        """
        Assign a new field limit function to the driver

        Args:
            limit_func: must be a function mapping (Bx, By, Bz) -> True/False
              where True means that the field is INSIDE the allowed region
        """

        # first check that the current target is allowed
        if not limit_func(*self._target_vector.get_components('x', 'y', 'z')):
            raise ValueError('Can not assign new limit function; present '
                             'target is illegal. Please change the target '
                             'and try again.')

        self._field_limits = limit_func

    def ramp(self, mode: str = "safe") -> None:
        """
        Ramp the fields to their present target value

        Args:
            mode: how to ramp, either 'simul', 'simul-block' or 'safe'. In
              'simul' and 'simul-block' mode, the fields are ramping
              simultaneously in a non-blocking mode and blocking mode,
              respectively. There is no safety check that the safe zone is not
              exceeded. In 'safe' mode, the fields are ramped one-by-one in a
              blocking way that ensures that the total field stays within the
              safe region (provided that this region is convex).
        """
        if mode not in ['simul', 'safe', 'simul_block']:
            raise ValueError('Invalid ramp mode. Please provide either "simul"'
                             ',"safe" or "simul_block".')

        meas_vals = self._get_measured(['x', 'y', 'z'])
        # we asked for three coordinates, so we know that we got a list
        meas_vals = cast(List[float], meas_vals)

        for cur, slave in zip(meas_vals, self.submodules.values()):
            if slave.field_target() != cur:
                if slave.field_ramp_rate() == 0:
                    raise ValueError(f'Can not ramp {slave}; ramp rate set to'
                                     ' zero!')

        # then the actual ramp
        {
            'simul': self._ramp_simultaneously,
            'safe': self._ramp_safely,
            'simul_block': self._ramp_simultaneously_blocking
        }[mode]()

    def _set_target_and_ramp(self, coordinate: str, mode: str,
                             target: float) -> None:
        """Convenient method to combine setting target and ramping"""
        self._set_target(coordinate, target)
        self.ramp(mode)

    def ask(self, cmd: str) -> str:
        """
        Since Oxford Instruments implement their own version of a SCPI-like
        language, we implement our own reader. Note that this command is used
        for getting and setting (asking and writing) alike.

        Args:
            cmd: the command to send to the instrument
        """

        visalog.debug(f"Writing to instrument {self.name}: {cmd}")
        resp = self.visa_handle.query(cmd)
        visalog.debug(f"Got instrument response: {resp}")

        if 'INVALID' in resp:
            log.error('Invalid command. Got response: {}'.format(resp))
            base_resp = resp
        # if the command was not invalid, it can either be a SET or a READ
        # SET:
        elif resp.endswith('VALID'):
            base_resp = resp.split(':')[-2]
        # READ:
        else:
            # For "normal" commands only (e.g. '*IDN?' is excepted):
            # the response of a valid command echoes back said command,
            # thus we remove that part
            base_cmd = cmd.replace('READ:', '')
            base_resp = resp.replace('STAT:{}'.format(base_cmd), '')

        return base_resp