def test_homogeneous_roundtrip(cartesian0): vec = FieldVector(**dict(zip("xyz", cartesian0))) h_vec = 13 * vec.as_homogeneous() assert np.allclose( vec.get_components(*"xyz"), FieldVector.from_homogeneous(h_vec).get_components(*"xyz"))
def test_triangle_inequality(cylindrical0, spherical0): cylindrical0 = FieldVector(**dict(zip(["rho", "phi", "z"], cylindrical0))) spherical0 = FieldVector(**dict(zip(["r", "phi", "theta"], spherical0))) assert (cylindrical0 + spherical0).norm() \ <= (cylindrical0.norm() + spherical0.norm()) assert cylindrical0.distance(spherical0) \ <= (cylindrical0.norm() + spherical0.norm())
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 test_cylindrical_properties(cylindrical0): """ If the cartesian to cylindrical transform has been correctly implemented then we expect certain symmetrical properties """ # Generate a random coordinate in cylindrical representation cylindrical0 = dict(zip(["rho", "phi", "z"], cylindrical0)) cartisian0 = FieldVector(**cylindrical0).get_components("x", "y", "z") # If we flip the sign of the rho coordinate, we will flip the xy coordinate cylindrical1 = dict(cylindrical0) cylindrical1["rho"] *= -1 cartisian1 = FieldVector(**cylindrical1).get_components("x", "y", "z") assert np.allclose(cartisian0, cartisian1 * np.array([-1, -1, 1]))
def _get_target_field(self) -> FieldVector: return FieldVector( **{ coord: self._get_component(coord) for coord in 'xyz' } )
def test_cartesian_setpoints(current_driver, set_target): """ Check that the individual x, y, z instruments are getting the set points as intended. This test is very similar to the sanity test, but adds in the FieldVector as well. """ current_driver.cartesian(set_target) x = current_driver.x() y = current_driver.y() z = current_driver.z() get_target = dict(zip(('x', 'y', 'z'), (x, y, z))) set_vector = FieldVector(*set_target) get_vector = FieldVector(**get_target) assert set_vector.is_equal(get_vector)
def test_json_dump(spherical0): vec = FieldVector(**dict(zip(["r", "phi", "theta"], spherical0))) dump = json.dumps(vec, cls=NumpyJSONEncoder) assert json.loads(dump) == { '__class__': FieldVector.__name__, '__args__': [vec.x, vec.y, vec.z] }
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"))
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
def test_spherical_setpoints(current_driver, set_target): """ Check that the individual x, y, z instruments are getting the set points as intended. This test is very similar to the sanity test, but adds in the FieldVector as well. """ current_driver.spherical(set_target) r = current_driver.field() theta = current_driver.theta() phi = current_driver.phi() get_target = dict(zip(('r', 'theta', 'phi'), (r, theta, phi))) set_target = dict(zip(('r', 'theta', 'phi'), set_target)) set_vector = FieldVector(**set_target) get_vector = FieldVector(**get_target) assert set_vector.is_equal(get_vector)
def test_cylindrical_setpoints(current_driver, set_target): """ Check that the individual x, y, z instruments are getting the set points as intended. This test is very similar to the sanity test, but adds in the FieldVector as well. """ current_driver.cylindrical(set_target) rho = current_driver.rho() z = current_driver.z() phi = current_driver.phi() get_target = dict(zip(('rho', 'phi', 'z'), (rho, phi, z))) set_target = dict(zip(('rho', 'phi', 'z'), set_target)) set_vector = FieldVector(**set_target) get_vector = FieldVector(**get_target) assert set_vector.is_equal(get_vector)
def test_all_attributes_are_floats(): cartesian0 = (400, 200, 300) cylindrical0 = (1, 52, 0) spherical0 = (1, 78, 145) cartesian = FieldVector(**dict(zip("xyz", cartesian0))) cylindrical = FieldVector(**dict(zip(["rho", "phi", "z"], cylindrical0))) spherical = FieldVector(**dict(zip(["r", "phi", "theta"], spherical0))) # Test that all attributes are floats upon creation for fv in [cartesian, cylindrical, spherical]: for attr in FieldVector.attributes: assert isinstance(getattr(fv, attr), float) # Test that all attributes are floats even after setting components for fv in [cartesian, cylindrical, spherical]: for set_comp in FieldVector.attributes: fv.set_component(**{set_comp: 1}) for attr in FieldVector.attributes: assert isinstance(getattr(fv, attr), float)
def test_spherical_properties(spherical0): """ If the cartesian to spherical transform has been correctly implemented then we expect certain symmetrical properties """ # Generate a random coordinate in spherical representation spherical0 = dict(zip(["r", "theta", "phi"], spherical0)) cartisian0 = FieldVector(**spherical0).get_components("x", "y", "z") # Mirror the theta angle in the xy-plane. # This should flip the sign of the z-coordinate spherical1 = dict(spherical0) spherical1["theta"] = 180 - spherical0["theta"] cartisian1 = FieldVector(**spherical1).get_components("x", "y", "z") assert np.allclose(cartisian0, cartisian1 * np.array([1, 1, -1])) # Add 180 to the phi coordinate. # This should flip the sign of the xy coordinate spherical2 = dict(spherical0) spherical2["phi"] = 180 + spherical0["phi"] cartisian2 = FieldVector(**spherical2).get_components("x", "y", "z") assert np.allclose(cartisian0, cartisian2 * np.array([-1, -1, 1])) # Mirroring the theta angle in the xy-plane # and adding 180 to the phi coordinate # should flip all cartesian coordinates spherical3 = dict(spherical0) spherical3["theta"] = 180 - spherical0["theta"] # should only flip z spherical3["phi"] = 180 + spherical0["phi"] # should flip xy cartisian3 = FieldVector(**spherical3).get_components("x", "y", "z") assert np.allclose(cartisian0, cartisian3 * np.array([-1, -1, -1])) # Finally, flipping the sign of the r coordinate # should flip all cartesian coordinates spherical4 = dict(spherical0) spherical4["r"] = -spherical0["r"] # This should flip all coordinates cartisian4 = FieldVector(**spherical4).get_components("x", "y", "z") assert np.allclose(cartisian0, cartisian4 * np.array([-1, -1, -1]))
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 _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 test_ramp_safely(driver, x, y, z, caplog): """ Test that we get the first-down-then-up order right """ # reset the instrument to default driver.GRPX.ramp_status('HOLD') driver.GRPY.ramp_status('HOLD') driver.GRPZ.ramp_status('HOLD') # the current field values are always zero for the sim. instr. # Use the FieldVector interface here to increase coverage. driver.field_target(FieldVector(x=x, y=y, z=z)) exp_order = \ np.array(['x', 'y', 'z'])[np.argsort(np.abs(np.array([x, y, z])))] with caplog.at_level(logging.DEBUG, logger='qcodes.instrument.visa'): caplog.clear() driver._ramp_safely() ramp_order = get_ramp_order(caplog.records) assert ramp_order == list(exp_order)
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 workers cartesian_targ = self._target_vector.get_components('x', 'y', 'z') for targ, worker in zip(cartesian_targ, self.submodules.values()): if not isinstance(worker, MercuryWorkerPS): raise RuntimeError(f"Expected a MercuryWorkerPS but got " f"{type(worker)}") worker.field_target(targ)
def test_measured(current_driver, set_target): """ Simply call the measurement methods and verify that no exceptions are raised. """ current_driver.cartesian(set_target) cartesian = current_driver.cartesian_measured() cartesian_x = current_driver.x_measured() cartesian_y = current_driver.y_measured() cartesian_z = current_driver.z_measured() assert np.allclose(cartesian, [cartesian_x, cartesian_y, cartesian_z]) assert FieldVector(*set_target).is_equal(FieldVector(x=cartesian_x, y=cartesian_y, z=cartesian_z)) spherical = current_driver.spherical_measured() spherical_field = current_driver.field_measured() spherical_theta = current_driver.theta_measured() spherical_phi = current_driver.phi_measured() assert FieldVector(*set_target).is_equal(FieldVector( r=spherical_field, theta=spherical_theta, phi=spherical_phi) ) assert np.allclose(spherical, [spherical_field, spherical_theta, spherical_phi]) cylindrical = current_driver.cylindrical_measured() cylindrical_rho = current_driver.rho_measured() assert FieldVector(*set_target).is_equal(FieldVector(rho=cylindrical_rho, phi=spherical_phi, z=cartesian_z)) assert np.allclose(cylindrical, [cylindrical_rho, spherical_phi, cartesian_z])
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): 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 cast(AMI430, 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())
def calculate_vector_ramp_rate_from_duration(start: FieldVector, setpoint: FieldVector, duration: float) -> float: return setpoint.distance(start) / duration
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_field(self) -> FieldVector: return FieldVector( x=self.x_measured(), y=self.y_measured(), z=self.z_measured() )
def test_vector_setting(driver): assert driver.field_target().distance(FieldVector(0, 0, 0)) <= 1e-8 driver.field_target(FieldVector(r=0.1, theta=0, phi=0)) assert driver.field_target().distance( FieldVector(r=0.1, theta=0, phi=0) ) <= 1e-8
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(), )
class AMI430_3D(Instrument): def __init__(self, name: str, instrument_x: AMI430, instrument_y: AMI430, instrument_z: AMI430, field_limit: Union[numbers.Real, Iterable[CartesianFieldLimitFunction]], **kwargs: Any): 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 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() ) def _verify_safe_setpoint( self, setpoint_values: Tuple[numbers.Real, numbers.Real, numbers.Real] ) -> bool: if isinstance(self._field_limit, 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[numbers.Real, numbers.Real, numbers.Real] ) -> 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") 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, 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 _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( self, *names: str ) -> Union[numbers.Real, List[numbers.Real]]: 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: 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[numbers.Real] ) -> 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
def __init__(self, name: str, address: str, visalib: Optional[str] = None, field_limits: Optional[Callable[[float, float, float], bool]] = None, **kwargs: Any) -> 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 = MercuryWorkerPS(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 test_vector_ramp_rate(driver): driver.field_ramp_rate(FieldVector(0.1, 0.1, 0.1)) assert driver.field_ramp_rate().distance( FieldVector(0.1, 0.1, 0.1) ) <= 1e-8
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 cast(AMI430, 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() )
def __init__(self, name: str, instrument_x: AMI430, instrument_y: AMI430, instrument_z: AMI430, field_limit: Union[numbers.Real, Iterable[CartesianFieldLimitFunction]], **kwargs: Any): 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 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() )
class MercuryiPS(VisaInstrument): """ Driver class for the QCoDeS Oxford Instruments MercuryiPS magnet power supply """ def __init__(self, name: str, address: str, visalib: Optional[str] = None, field_limits: Optional[Callable[[float, float, float], bool]] = None, **kwargs: Any) -> 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 = MercuryWorkerPS(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 workers cartesian_targ = self._target_vector.get_components('x', 'y', 'z') for targ, worker in zip(cartesian_targ, self.submodules.values()): if not isinstance(worker, MercuryWorkerPS): raise RuntimeError(f"Expected a MercuryWorkerPS but got " f"{type(worker)}") worker.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 worker in self.submodules.values(): if not isinstance(worker, MercuryWorkerPS): raise RuntimeError(f"Expected a MercuryWorkerPS but got " f"{type(worker)}") worker.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 worker in self.submodules.values(): if not isinstance(worker, MercuryWorkerPS): raise RuntimeError(f"Expected a MercuryWorkerPS but got " f"{type(worker)}") # wait for the ramp to finish, we don't care about the order while worker.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 worker in np.array(list(self.submodules.values()))[order]: worker.ramp_to_target() # now just wait for the ramp to finish # (unless we are testing) if self.visabackend == 'sim': pass else: while worker.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'] _ = 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[[float, float, float], bool]) -> 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, worker in zip(meas_vals, self.submodules.values()): if not isinstance(worker, MercuryWorkerPS): raise RuntimeError(f"Expected a MercuryWorkerPS but got " f"{type(worker)}") if worker.field_target() != cur: if worker.field_ramp_rate() == 0: raise ValueError(f'Can not ramp {worker}; 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(f'Invalid command. Got response: {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(f'STAT:{base_cmd}', '') return base_resp