From e4b40974beca4df6a413c863d9ab606317d102ef Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 4 Feb 2026 08:37:39 +0100 Subject: [PATCH 01/11] Fix most issues in AMI430 --- .../american_magnetics/AMI430_visa.py | 61 +++++++++++++------ 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/src/qcodes/instrument_drivers/american_magnetics/AMI430_visa.py b/src/qcodes/instrument_drivers/american_magnetics/AMI430_visa.py index 3d701d54b12..1a294b50191 100644 --- a/src/qcodes/instrument_drivers/american_magnetics/AMI430_visa.py +++ b/src/qcodes/instrument_drivers/american_magnetics/AMI430_visa.py @@ -45,7 +45,7 @@ class AMI430Warning(UserWarning): pass -class AMI430SwitchHeater(InstrumentChannel): +class AMI430SwitchHeater(InstrumentChannel["AMIModel430"]): class _Decorators: @classmethod def check_enabled( @@ -138,14 +138,14 @@ def _check_enabled(self) -> bool: @_Decorators.check_enabled def _on(self) -> None: self.write("PS 1") - while self._parent.ramping_state() == "heating switch": - self._parent._sleep(0.5) + while self.parent.ramping_state() == "heating switch": + self.parent._sleep(0.5) @_Decorators.check_enabled def _off(self) -> None: self.write("PS 0") - while self._parent.ramping_state() == "cooling switch": - self._parent._sleep(0.5) + while self.parent.ramping_state() == "cooling switch": + self.parent._sleep(0.5) def _check_state(self) -> bool: if self.enabled() is False: @@ -220,8 +220,6 @@ def __init__( self._parent_instrument = None - # Add reset function - self.add_function("reset", call_cmd="*RST") if reset: self.reset() @@ -251,8 +249,8 @@ def __init__( """Parameter current_ramp_limit""" self.field_ramp_limit: Parameter = self.add_parameter( "field_ramp_limit", - get_cmd=self.current_ramp_limit, - set_cmd=self.current_ramp_limit, + get_cmd=self.current_ramp_limit.get, + set_cmd=self.current_ramp_limit.set, scale=1 / float(self.ask("COIL?")), unit="T/s", ) @@ -309,8 +307,7 @@ def __init__( "is_quenched", get_cmd="QU?", val_mapping={True: 1, False: 0} ) """Parameter is_quenched""" - self.add_function("reset_quench", call_cmd="QU 0") - self.add_function("set_quenched", call_cmd="QU 1") + self.ramping_state: Parameter = self.add_parameter( "ramping_state", get_cmd="STATE?", @@ -345,17 +342,39 @@ def __init__( ) """Submodule the switch heater submodule.""" - # Add interaction functions - self.add_function("get_error", call_cmd="SYST:ERR?") - self.add_function("ramp", call_cmd="RAMP") - self.add_function("pause", call_cmd="PAUSE") - self.add_function("zero", call_cmd="ZERO") - # Correctly assign all units self._update_units() self.connect_message() + def get_error(self) -> str: + """Get the last error from the instrument""" + return self.ask("SYST:ERR?") + + def ramp(self) -> None: + """Start ramping to the setpoint""" + self.write("RAMP") + + def pause(self) -> None: + """Pause ramping""" + self.write("PAUSE") + + def zero(self) -> None: + """Ramp to zero current""" + self.write("ZERO") + + def reset_quench(self) -> None: + """Reset a quench condition on the instrument""" + self.write("QU 0") + + def set_quenched(self) -> None: + """Set a quench condition on the instrument""" + self.write("QU 1") + + def reset(self) -> None: + """Reset the instrument to default settings""" + self.write("*RST") + def _sleep(self, t: float) -> None: """ Sleep for a number of seconds t. If we are or using @@ -1034,8 +1053,12 @@ def _adjust_child_instruments(self, values: tuple[float, float, float]) -> None: raise ValueError("_set_fields aborted; field would exceed limit") # Check if the individual instruments are ready - for name in ("x", "y", "z"): - instrument = getattr(self, f"_instrument_{name}") + Instruments_to_check = ( + self._instrument_x, + self._instrument_y, + self._instrument_z, + ) + for instrument in Instruments_to_check: if instrument.ramping_state() == "ramping": msg = f"_set_fields aborted; magnet {instrument} is already ramping" raise AMI430Exception(msg) From cc0102959de09e7ba5568d6e6fc182376158a393 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 12 Mar 2026 09:58:43 +0100 Subject: [PATCH 02/11] Add todo --- src/qcodes/instrument_drivers/american_magnetics/AMI430_visa.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/qcodes/instrument_drivers/american_magnetics/AMI430_visa.py b/src/qcodes/instrument_drivers/american_magnetics/AMI430_visa.py index 1a294b50191..85d42fc01e0 100644 --- a/src/qcodes/instrument_drivers/american_magnetics/AMI430_visa.py +++ b/src/qcodes/instrument_drivers/american_magnetics/AMI430_visa.py @@ -1204,6 +1204,7 @@ def _request_field_change(self, instrument: AMIModel430, value: NumberType) -> N individually. It results in additional safety checks being performed by this 3D driver. """ + # TODO the _set_x _set_y _set_z methods don't seem to exist anywhere if instrument is self._instrument_x: self._set_x(value) elif instrument is self._instrument_y: From 648704122d240edf2ba67c7b17dba58a7f311503 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 6 Apr 2026 10:02:21 +0200 Subject: [PATCH 03/11] Ensure _request_field_change works correctly --- .../american_magnetics/AMI430_visa.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/qcodes/instrument_drivers/american_magnetics/AMI430_visa.py b/src/qcodes/instrument_drivers/american_magnetics/AMI430_visa.py index 85d42fc01e0..eaa420bde55 100644 --- a/src/qcodes/instrument_drivers/american_magnetics/AMI430_visa.py +++ b/src/qcodes/instrument_drivers/american_magnetics/AMI430_visa.py @@ -218,7 +218,7 @@ def __init__( # read the hello part of the welcome message self.visa_handle.read() - self._parent_instrument = None + self._parent_instrument: AMIModel4303D | None = None if reset: self.reset() @@ -681,6 +681,10 @@ def find_ami430_with_name(ami430_name: str) -> AMIModel430: else find_ami430_with_name(instrument_z) ) + self._instrument_x._parent_instrument = self + self._instrument_y._parent_instrument = self + self._instrument_z._parent_instrument = self + self._field_limit: float | Iterable[CartesianFieldLimitFunction] if isinstance(field_limit, Iterable): self._field_limit = field_limit @@ -1204,13 +1208,12 @@ def _request_field_change(self, instrument: AMIModel430, value: NumberType) -> N individually. It results in additional safety checks being performed by this 3D driver. """ - # TODO the _set_x _set_y _set_z methods don't seem to exist anywhere if instrument is self._instrument_x: - self._set_x(value) + self._set_setpoints(("x",), (float(value),)) elif instrument is self._instrument_y: - self._set_y(value) + self._set_setpoints(("y",), (float(value),)) elif instrument is self._instrument_z: - self._set_z(value) + self._set_setpoints(("z",), (float(value),)) else: msg = "This magnet doesnt belong to its specified parent {}" raise NameError(msg.format(self)) From a7e2319485e84ff3be684ad5e1c780f554467a07 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 6 Apr 2026 10:09:40 +0200 Subject: [PATCH 04/11] Add tests for ami430 parent setting logic --- tests/drivers/test_ami430_visa.py | 115 ++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/tests/drivers/test_ami430_visa.py b/tests/drivers/test_ami430_visa.py index b5bd21a623b..e7edf5789d1 100644 --- a/tests/drivers/test_ami430_visa.py +++ b/tests/drivers/test_ami430_visa.py @@ -140,6 +140,121 @@ def test_instantiation_from_names( assert driver._instrument_z is mag_z +def test_parent_instrument_is_set_on_child_axes(current_driver) -> None: + """ + Test that after creating AMIModel4303D, the _parent_instrument attribute + on each child axis instrument is set to the 3D driver instance. + """ + assert current_driver._instrument_x._parent_instrument is current_driver + assert current_driver._instrument_y._parent_instrument is current_driver + assert current_driver._instrument_z._parent_instrument is current_driver + + +def test_parent_instrument_is_none_without_3d_driver(ami430) -> None: + """ + Test that a standalone AMIModel430 that is not part of a 3D driver + has _parent_instrument set to None. + """ + assert ami430._parent_instrument is None + + +def test_request_field_change_via_child_set_field(current_driver) -> None: + """ + Test that calling set_field on a child instrument that belongs to a 3D + driver delegates to the 3D driver's _request_field_change, which routes + through _set_setpoints and performs safety checks. The 3D driver's + internal _set_point should be updated accordingly. + """ + # Start from a known state + current_driver.cartesian((0.0, 0.0, 0.0)) + + # Set field directly on the child x instrument + current_driver._instrument_x.set_field(0.5) + + # The 3D driver's setpoint should reflect the change + assert np.isclose(current_driver.x(), 0.5) + # y and z should remain at 0 + assert np.isclose(current_driver.y(), 0.0) + assert np.isclose(current_driver.z(), 0.0) + + +def test_request_field_change_respects_field_limits(current_driver) -> None: + """ + Test that calling set_field on a child instrument still respects + the 3D driver's field limits when _request_field_change delegates + to _set_setpoints -> _adjust_child_instruments -> _verify_safe_setpoint. + """ + # Set a state that is near the field limit boundary + current_driver.cartesian((1.0, 1.0, 0.0)) + + # Try to set z to a value that would exceed the field limit + # field_limit says norm < 2, so (1.0, 1.0, 1.5) has norm ~2.06 which is unsafe + # and it doesn't satisfy x==0 and y==0 for the z<3 rule either + with pytest.raises(ValueError, match="field would exceed limit"): + current_driver._instrument_z.set_field(1.5) + + +def test_request_field_change_for_each_axis(current_driver) -> None: + """ + Test that _request_field_change correctly routes for each axis (x, y, z) + by setting fields on each child instrument individually. + """ + current_driver.cartesian((0.0, 0.0, 0.0)) + + current_driver._instrument_x.set_field(0.3) + assert np.isclose(current_driver.x(), 0.3) + + current_driver._instrument_y.set_field(0.4) + assert np.isclose(current_driver.y(), 0.4) + + current_driver._instrument_z.set_field(0.5) + assert np.isclose(current_driver.z(), 0.5) + + # All three should now reflect the individually-set values + assert np.allclose(current_driver.cartesian(), [0.3, 0.4, 0.5]) + + +def test_request_field_change_unknown_instrument_raises( + current_driver, request: FixtureRequest +) -> None: + """ + Test that _request_field_change raises a NameError when called with + an instrument that is not one of the x/y/z child instruments. + """ + request.addfinalizer(Instrument.close_all) + stranger = AMIModel430( + "stranger", + address="GPIB::4::INSTR", + pyvisa_sim_file="AMI430.yaml", + terminator="\n", + ) + + with pytest.raises(NameError, match="doesnt belong to its specified parent"): + current_driver._request_field_change(stranger, 0.5) + + +def test_child_set_field_bypasses_parent_when_safety_check_false( + current_driver, +) -> None: + """ + Test that calling set_field with perform_safety_check=False on a child + instrument does NOT delegate to the parent 3D driver, even when + _parent_instrument is set. This is the path used internally by the + 3D driver's _perform_default_ramp and _perform_simultaneous_ramp. + """ + current_driver.cartesian((0.0, 0.0, 0.0)) + + # Directly set field bypassing parent safety check + current_driver._instrument_x.set_field(0.5, perform_safety_check=False) + + # The child instrument should have ramped, but the 3D driver's + # internal _set_point should NOT have been updated (since we + # bypassed the parent) + assert np.isclose(current_driver._instrument_x.field(), 0.5) + # The 3D driver's x setpoint should still be 0.0 + assert np.isclose(current_driver.x(), 0.0) + + def test_visa_interaction(request: FixtureRequest) -> None: """ Test that closing one instrument we can still use the other simulated instruments. From 4154df60f388f006b67f7326969dc4c96f8fc697 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 6 Apr 2026 10:12:06 +0200 Subject: [PATCH 05/11] Add changelog for 7923 --- docs/changes/newsfragments/7923.improved_driver | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 docs/changes/newsfragments/7923.improved_driver diff --git a/docs/changes/newsfragments/7923.improved_driver b/docs/changes/newsfragments/7923.improved_driver new file mode 100644 index 00000000000..fe185206620 --- /dev/null +++ b/docs/changes/newsfragments/7923.improved_driver @@ -0,0 +1,4 @@ +Fixed ``AMIModel4303D`` to properly set ``_parent_instrument`` on child ``AMIModel430`` +instruments during initialization, enabling the safety-check delegation path in +``set_field``. Also fixed ``_request_field_change`` to correctly call ``_set_setpoints`` +instead of non-existent ``_set_x``/``_set_y``/``_set_z`` methods. From 0349072633dbbdca439325e618a4d10d6ad21428 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 6 Apr 2026 10:32:10 +0200 Subject: [PATCH 06/11] Improve tests --- .../newsfragments/7923.improved_driver | 2 + src/qcodes/instrument/sims/AMI430.yaml | 42 ++++++++------ tests/drivers/test_ami430_visa.py | 58 +++++++++++++++++++ 3 files changed, 83 insertions(+), 19 deletions(-) diff --git a/docs/changes/newsfragments/7923.improved_driver b/docs/changes/newsfragments/7923.improved_driver index fe185206620..05b5a0cf02e 100644 --- a/docs/changes/newsfragments/7923.improved_driver +++ b/docs/changes/newsfragments/7923.improved_driver @@ -1,3 +1,5 @@ +Converted ``get_error``, ``ramp``, ``pause``, ``zero``, ``reset_quench``, ``set_quenched``, +and ``reset`` on ``AMIModel430`` from QCoDeS functions (``add_function``) to proper methods. Fixed ``AMIModel4303D`` to properly set ``_parent_instrument`` on child ``AMIModel430`` instruments during initialization, enabling the safety-check delegation path in ``set_field``. Also fixed ``_request_field_change`` to correctly call ``_set_setpoints`` diff --git a/src/qcodes/instrument/sims/AMI430.yaml b/src/qcodes/instrument/sims/AMI430.yaml index 609ccc9de5f..f7073dce34d 100644 --- a/src/qcodes/instrument/sims/AMI430.yaml +++ b/src/qcodes/instrument/sims/AMI430.yaml @@ -1,7 +1,6 @@ spec: "1.0" devices: - device 1: eom: GPIB INSTR: @@ -11,9 +10,12 @@ devices: dialogues: - q: "*IDN?" r: "QCoDeS, AMI430_simulation, 1337, 0.0.01" + - q: "SYST:ERR?" + r: "0, No Error" + - q: "*RST" + r: "" properties: - ramp rate units: default: 0 getter: @@ -39,41 +41,41 @@ devices: q: "CONF:CURR:LIMIT {}" ramp rate current first segment: - default: '50, 50, 50' + default: "50, 50, 50" getter: - q: 'RAMP:RATE:CURRENT:1?' - r: '{}' + q: "RAMP:RATE:CURRENT:1?" + r: "{}" setter: - q: 'CONF:RAMP:RATE:CURRENT:1 {}' + q: "CONF:RAMP:RATE:CURRENT:1 {}" segment for ramp rate: # This is some segment value that gets set in the driver code # when ramp rate field parameter is set default: 1 setter: - q: 'CONF:RAMP:RATE:SEG {}' + q: "CONF:RAMP:RATE:SEG {}" ramp rate field first segment: - default: '0.11, 0.11, 0.11' + default: "0.11, 0.11, 0.11" getter: - q: 'RAMP:RATE:FIELD:1?' - r: '{}' + q: "RAMP:RATE:FIELD:1?" + r: "{}" setter: - q: 'CONF:RAMP:RATE:FIELD 1,{},0' + q: "CONF:RAMP:RATE:FIELD 1,{},0" ramp target: - default: 0 # or what? + default: 0 # or what? getter: q: "FIELD:TARG?" r: "{}" #setter: # this is commented out because two properties can not share a setter - # q: "CONF:FIELD:TARG {}" + # q: "CONF:FIELD:TARG {}" coil constant: default: 2.0 getter: - q: 'COIL?' - r: '{}' + q: "COIL?" + r: "{}" setter: q: "CONF:COIL {}" @@ -83,10 +85,10 @@ devices: q: "FIELD:MAG?" r: "{}" setter: - q: "CONF:FIELD:TARG {}" # in the simulated instrument, the target is reached + q: "CONF:FIELD:TARG {}" # in the simulated instrument, the target is reached ramping state: - default: 2 # always in holding state, always ready + default: 2 # always in holding state, always ready getter: q: "STATE?" r: "{}" @@ -137,7 +139,6 @@ devices: setter: q: "CONF:PS:CTIME {}" - pause: setter: q: "PAUSE" @@ -146,6 +147,10 @@ devices: setter: q: "RAMP" + zero: + setter: + q: "ZERO" + current rating: default: 3 getter: @@ -162,7 +167,6 @@ devices: setter: q: "CONF:PS {}" - # we always need three power supplies, one for each axis. # For the testing we add a few more. resources: diff --git a/tests/drivers/test_ami430_visa.py b/tests/drivers/test_ami430_visa.py index e7edf5789d1..3ed8ea1c6d9 100644 --- a/tests/drivers/test_ami430_visa.py +++ b/tests/drivers/test_ami430_visa.py @@ -1393,3 +1393,61 @@ def test_switch_heater_enabled(ami430, caplog) -> None: assert ami430.switch_heater.enabled() is True ami430.switch_heater.enabled(False) assert ami430.switch_heater.enabled() is False + + +def test_get_error(ami430) -> None: + """ + Test that get_error queries the instrument and returns a string response. + """ + result = ami430.get_error() + assert isinstance(result, str) + assert "No Error" in result + + +def test_ramp(ami430) -> None: + """ + Test that the ramp method sends the RAMP command without raising. + """ + ami430.ramp() + + +def test_pause(ami430) -> None: + """ + Test that the pause method sends the PAUSE command without raising. + """ + ami430.pause() + + +def test_zero(ami430) -> None: + """ + Test that the zero method sends the ZERO command without raising. + """ + ami430.zero() + + +def test_reset_quench(ami430) -> None: + """ + Test that reset_quench clears the quench condition on the instrument. + """ + ami430.set_quenched() + assert ami430.is_quenched() is True + ami430.reset_quench() + assert ami430.is_quenched() is False + + +def test_set_quenched(ami430) -> None: + """ + Test that set_quenched sets the quench condition on the instrument. + """ + assert ami430.is_quenched() is False + ami430.set_quenched() + assert ami430.is_quenched() is True + # Clean up + ami430.reset_quench() + + +def test_reset(ami430) -> None: + """ + Test that the reset method sends the *RST command without raising. + """ + ami430.reset() From 77b9641761c605fdf9ce2afd60606b388350ccb5 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 6 Apr 2026 10:37:06 +0200 Subject: [PATCH 07/11] Handle zero method correctly --- .../newsfragments/7923.improved_driver | 2 + .../american_magnetics/AMI430_visa.py | 16 ++++++ tests/drivers/test_ami430_visa.py | 49 ++++++++++++++++++- 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/docs/changes/newsfragments/7923.improved_driver b/docs/changes/newsfragments/7923.improved_driver index 05b5a0cf02e..01b1cc2b0cc 100644 --- a/docs/changes/newsfragments/7923.improved_driver +++ b/docs/changes/newsfragments/7923.improved_driver @@ -1,5 +1,7 @@ Converted ``get_error``, ``ramp``, ``pause``, ``zero``, ``reset_quench``, ``set_quenched``, and ``reset`` on ``AMIModel430`` from QCoDeS functions (``add_function``) to proper methods. +The ``zero`` method now invalidates the ``field`` parameter cache and updates the parent +``AMIModel4303D`` driver's internal setpoint tracking when called on a child instrument. Fixed ``AMIModel4303D`` to properly set ``_parent_instrument`` on child ``AMIModel430`` instruments during initialization, enabling the safety-check delegation path in ``set_field``. Also fixed ``_request_field_change`` to correctly call ``_set_setpoints`` diff --git a/src/qcodes/instrument_drivers/american_magnetics/AMI430_visa.py b/src/qcodes/instrument_drivers/american_magnetics/AMI430_visa.py index eaa420bde55..181023cbe49 100644 --- a/src/qcodes/instrument_drivers/american_magnetics/AMI430_visa.py +++ b/src/qcodes/instrument_drivers/american_magnetics/AMI430_visa.py @@ -362,6 +362,9 @@ def pause(self) -> None: def zero(self) -> None: """Ramp to zero current""" self.write("ZERO") + self.field.cache.invalidate() + if self._parent_instrument is not None: + self._parent_instrument._update_setpoint_on_child_zero(self) def reset_quench(self) -> None: """Reset a quench condition on the instrument""" @@ -1202,6 +1205,19 @@ def pause(self) -> None: ): axis_instrument.pause() + def _update_setpoint_on_child_zero(self, instrument: AMIModel430) -> None: + """ + Update the internal ``_set_point`` when a child instrument's + ``zero()`` method is called directly, so that the 3D driver's + setpoint tracking remains consistent. + """ + if instrument is self._instrument_x: + self._set_point.set_component(x=0.0) + elif instrument is self._instrument_y: + self._set_point.set_component(y=0.0) + elif instrument is self._instrument_z: + self._set_point.set_component(z=0.0) + def _request_field_change(self, instrument: AMIModel430, value: NumberType) -> None: """ This method is called by the child x/y/z magnets if they are set diff --git a/tests/drivers/test_ami430_visa.py b/tests/drivers/test_ami430_visa.py index 3ed8ea1c6d9..cfeed3fd036 100644 --- a/tests/drivers/test_ami430_visa.py +++ b/tests/drivers/test_ami430_visa.py @@ -1420,10 +1420,57 @@ def test_pause(ami430) -> None: def test_zero(ami430) -> None: """ - Test that the zero method sends the ZERO command without raising. + Test that the zero method sends the ZERO command without raising + and invalidates the field parameter cache. """ + # Set the field to a non-zero value so the cache is populated + ami430.field(0.5) + assert ami430.field.cache.valid is True + ami430.zero() + # The field cache should be invalidated after zeroing + assert ami430.field.cache.valid is False + + +def test_zero_updates_parent_setpoint(current_driver) -> None: + """ + Test that calling zero() on a child instrument updates the parent + 3D driver's internal _set_point so that setpoint tracking remains + consistent. + """ + current_driver.cartesian((0.5, 0.6, 0.7)) + assert np.allclose(current_driver.cartesian(), [0.5, 0.6, 0.7]) + + # Zero the x axis via the child instrument + current_driver._instrument_x.zero() + + # The parent's setpoint should reflect x=0 while y and z remain + assert np.isclose(current_driver.x(), 0.0) + assert np.isclose(current_driver.y(), 0.6) + assert np.isclose(current_driver.z(), 0.7) + + +def test_zero_updates_parent_setpoint_each_axis(current_driver) -> None: + """ + Test that calling zero() on each child axis individually updates the + corresponding component of the parent 3D driver's _set_point. + """ + current_driver.cartesian((0.3, 0.4, 0.5)) + + current_driver._instrument_y.zero() + assert np.isclose(current_driver.y(), 0.0) + assert np.isclose(current_driver.x(), 0.3) + assert np.isclose(current_driver.z(), 0.5) + + current_driver._instrument_z.zero() + assert np.isclose(current_driver.z(), 0.0) + assert np.isclose(current_driver.x(), 0.3) + assert np.isclose(current_driver.y(), 0.0) + + current_driver._instrument_x.zero() + assert np.allclose(current_driver.cartesian(), [0.0, 0.0, 0.0]) + def test_reset_quench(ami430) -> None: """ From e9c232d09153fd42855d1bbb0a35cc98e521b253 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 6 Apr 2026 10:53:50 +0200 Subject: [PATCH 08/11] Improve test coverage for ami430 --- src/qcodes/instrument/sims/AMI430.yaml | 4 + tests/drivers/test_ami430_visa.py | 144 +++++++++++++++++++++++++ 2 files changed, 148 insertions(+) diff --git a/src/qcodes/instrument/sims/AMI430.yaml b/src/qcodes/instrument/sims/AMI430.yaml index f7073dce34d..bbb96f54041 100644 --- a/src/qcodes/instrument/sims/AMI430.yaml +++ b/src/qcodes/instrument/sims/AMI430.yaml @@ -92,6 +92,8 @@ devices: getter: q: "STATE?" r: "{}" + setter: + q: "CONF:STATE {}" quench state: default: 0 @@ -106,6 +108,8 @@ devices: getter: q: "PERS?" r: "{}" + setter: + q: "CONF:PERS {}" persistent heater state: default: 0 diff --git a/tests/drivers/test_ami430_visa.py b/tests/drivers/test_ami430_visa.py index cfeed3fd036..ac4e8cc9f9a 100644 --- a/tests/drivers/test_ami430_visa.py +++ b/tests/drivers/test_ami430_visa.py @@ -14,6 +14,7 @@ from qcodes.instrument import Instrument from qcodes.instrument_drivers.american_magnetics import ( + AMI430Exception, AMI430Warning, AMIModel430, AMIModel4303D, @@ -1498,3 +1499,146 @@ def test_reset(ami430) -> None: Test that the reset method sends the *RST command without raising. """ ami430.reset() + + +def test_can_start_ramping_returns_false_when_quenched(ami430) -> None: + """ + Test that _can_start_ramping returns False when the instrument is in + a quench condition, and that set_field raises accordingly. + """ + ami430.set_quenched() + assert ami430.is_quenched() is True + assert ami430._can_start_ramping() is False + + with pytest.raises(AMI430Exception, match="Cannot ramp in current state"): + ami430.set_field(0.5, perform_safety_check=False) + + # Clean up + ami430.reset_quench() + + +def test_can_start_ramping_returns_false_when_persistent(ami430) -> None: + """ + Test that _can_start_ramping returns False when the instrument is in + persistent mode, and that set_field raises accordingly. + """ + # Put the instrument into persistent mode via the simulator + ami430.write("CONF:PERS 1") + assert ami430.switch_heater.in_persistent_mode() is True + assert ami430._can_start_ramping() is False + + with pytest.raises(AMI430Exception, match="Cannot ramp in current state"): + ami430.set_field(0.5, perform_safety_check=False) + + # Clean up + ami430.write("CONF:PERS 0") + + +def test_can_start_ramping_returns_false_in_unexpected_state(ami430) -> None: + """ + Test that _can_start_ramping returns False when the instrument is in + a state that is not 'holding', 'paused', 'at zero current', or + 'ramping' (e.g. 'manual up', 'zeroing current'). + """ + # "manual up" is state 4 + ami430.write("CONF:STATE 4") + assert ami430.ramping_state() == "manual up" + assert ami430._can_start_ramping() is False + + # "zeroing current" is state 6 + ami430.write("CONF:STATE 6") + assert ami430.ramping_state() == "zeroing current" + assert ami430._can_start_ramping() is False + + # Clean up: restore to holding (state 2) + ami430.write("CONF:STATE 2") + + +def test_can_start_ramping_returns_true_when_holding(ami430) -> None: + """ + Test that _can_start_ramping returns True when the instrument is in + the 'holding' state (the default simulator state). + """ + assert ami430.ramping_state() == "holding" + assert ami430._can_start_ramping() is True + + +def test_can_start_ramping_returns_true_when_paused(ami430) -> None: + """ + Test that _can_start_ramping returns True when the instrument is in + the 'paused' state. + """ + ami430.write("CONF:STATE 3") + assert ami430.ramping_state() == "paused" + assert ami430._can_start_ramping() is True + + # Clean up + ami430.write("CONF:STATE 2") + + +def test_can_start_ramping_returns_true_when_at_zero_current(ami430) -> None: + """ + Test that _can_start_ramping returns True when the instrument is in + the 'at zero current' state. + """ + ami430.write("CONF:STATE 8") + assert ami430.ramping_state() == "at zero current" + assert ami430._can_start_ramping() is True + + # Clean up + ami430.write("CONF:STATE 2") + + +def test_can_start_ramping_when_ramping_with_heater_disabled(ami430) -> None: + """ + Test that _can_start_ramping returns True when already ramping and + the switch heater is not enabled. + """ + ami430.write("CONF:STATE 1") + assert ami430.ramping_state() == "ramping" + assert ami430.switch_heater.enabled() is False + assert ami430._can_start_ramping() is True + + # Clean up + ami430.write("CONF:STATE 2") + + +def test_can_start_ramping_when_ramping_with_heater_enabled_and_on(ami430) -> None: + """ + Test that _can_start_ramping returns True when already ramping and + the switch heater is enabled and on (warm). + """ + ami430.write("CONF:STATE 1") + ami430.switch_heater.enabled(True) + ami430.write("PS 1") + + assert ami430.ramping_state() == "ramping" + assert ami430.switch_heater.enabled() is True + assert ami430.switch_heater.state() is True + assert ami430._can_start_ramping() is True + + # Clean up + ami430.write("PS 0") + ami430.switch_heater.enabled(False) + ami430.write("CONF:STATE 2") + + +def test_can_start_ramping_returns_false_when_ramping_with_heater_enabled_and_off( + ami430, +) -> None: + """ + Test that _can_start_ramping returns False when already ramping and + the switch heater is enabled but off (cold), meaning the persistent + switch is not heated. + """ + ami430.write("CONF:STATE 1") + ami430.switch_heater.enabled(True) + # Heater enabled but state is off (default) + assert ami430.switch_heater.state() is False + + assert ami430.ramping_state() == "ramping" + assert ami430._can_start_ramping() is False + + # Clean up + ami430.switch_heater.enabled(False) + ami430.write("CONF:STATE 2") From f36e7e4f2bb2a9e7f7843529859d5ff5e4773bbe Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 6 Apr 2026 11:30:37 +0200 Subject: [PATCH 09/11] Improve test coverage for ami430 --- tests/drivers/test_ami430_visa.py | 228 ++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) diff --git a/tests/drivers/test_ami430_visa.py b/tests/drivers/test_ami430_visa.py index ac4e8cc9f9a..8b54dc891cc 100644 --- a/tests/drivers/test_ami430_visa.py +++ b/tests/drivers/test_ami430_visa.py @@ -1642,3 +1642,231 @@ def test_can_start_ramping_returns_false_when_ramping_with_heater_enabled_and_of # Clean up ami430.switch_heater.enabled(False) ami430.write("CONF:STATE 2") + + +def test_switch_heater_on_raises_when_not_enabled(ami430) -> None: + """ + Test that turning the switch heater on when it is not enabled + raises an AMI430Exception. + """ + assert ami430.switch_heater.enabled() is False + with pytest.raises(AMI430Exception, match="Switch not enabled"): + ami430.switch_heater.state(True) + + +def test_switch_heater_off_raises_when_not_enabled(ami430) -> None: + """ + Test that turning the switch heater off when it is not enabled + raises an AMI430Exception. + """ + assert ami430.switch_heater.enabled() is False + with pytest.raises(AMI430Exception, match="Switch not enabled"): + ami430.switch_heater.state(False) + + +def test_switch_heater_on_when_enabled(ami430) -> None: + """ + Test that setting switch heater state to True succeeds when the + switch heater is enabled. + """ + ami430.switch_heater.enabled(True) + ami430.switch_heater.state(True) + assert ami430.switch_heater.state() is True + # Clean up + ami430.switch_heater.state(False) + ami430.switch_heater.enabled(False) + + +def test_switch_heater_off_when_enabled(ami430) -> None: + """ + Test that setting switch heater state to False succeeds when the + switch heater is enabled. + """ + ami430.switch_heater.enabled(True) + ami430.switch_heater.state(True) + ami430.switch_heater.state(False) + assert ami430.switch_heater.state() is False + ami430.switch_heater.enabled(False) + + +def test_ami430_init_with_reset(request: FixtureRequest, mocker) -> None: + """ + Test that AMIModel430 can be instantiated with reset=True + to cover the reset path during initialization. + + We mock the reset method because pyvisa-sim's ``*RST`` dialogue + leaves an empty response in the read buffer which corrupts + subsequent queries. + """ + request.addfinalizer(Instrument.close_all) + mock_reset = mocker.patch.object(AMIModel430, "reset") + mag = AMIModel430( + "ami430_reset", + address="GPIB::4::INSTR", + pyvisa_sim_file="AMI430.yaml", + terminator="\n", + reset=True, + ) + mock_reset.assert_called_once() + # The instrument should be functional after init + assert mag.field() is not None + + +def test_ami430_init_with_custom_current_ramp_limit( + request: FixtureRequest, +) -> None: + """ + Test that AMIModel430 can be instantiated with a custom + current_ramp_limit value. + """ + request.addfinalizer(Instrument.close_all) + custom_limit = 0.03 + mag = AMIModel430( + "ami430_custom_ramp", + address="GPIB::4::INSTR", + pyvisa_sim_file="AMI430.yaml", + terminator="\n", + current_ramp_limit=custom_limit, + ) + assert mag.current_ramp_limit() == custom_limit + + +def test_set_field_exceeding_field_limit(ami430) -> None: + """ + Test that set_field raises a ValueError when the requested field + exceeds the individual instrument's field limit (coil_constant * + current_limit). + """ + field_lim = float(ami430.ask("COIL?")) * ami430.current_limit() + with pytest.raises(ValueError, match="Aborted _set_field"): + ami430.set_field(field_lim + 1, perform_safety_check=False) + + +def test_set_field_raises_when_switch_heater_enabled_but_off(ami430) -> None: + """ + Test that set_field raises an AMI430Exception when the switch heater + is enabled but its state is off (cold), meaning the persistent switch + is not heated. + """ + ami430.switch_heater.enabled(True) + # Switch heater is enabled but off (default state is off) + assert ami430.switch_heater.state() is False + with pytest.raises(AMI430Exception, match="Switch heater is not on"): + ami430.set_field(0.5, perform_safety_check=False) + # Clean up + ami430.switch_heater.enabled(False) + + +def test_3d_driver_get_idn(current_driver) -> None: + """Test that AMIModel4303D.get_idn returns the expected IDN dict.""" + idn = current_driver.get_idn() + assert idn["vendor"] == "American Magnetics" + assert idn["model"] == "AMI430_3D" + assert idn["serial"] is None + assert idn["firmware"] is None + + +def test_3d_driver_invalid_field_limit_type( + magnet_axes_instances, request: FixtureRequest +) -> None: + """Test that passing an invalid field_limit type raises ValueError.""" + mag_x, mag_y, mag_z = magnet_axes_instances + request.addfinalizer(Instrument.close_all) + with pytest.raises(ValueError, match="field limit should either be a number"): + AMIModel4303D("AMI430_3D", mag_x, mag_y, mag_z, None) # type: ignore[arg-type] + + +def test_ramp_simultaneously(current_driver) -> None: + """Test the ramp_simultaneously method on AMIModel4303D.""" + current_driver.cartesian((0.0, 0.0, 0.0)) + setpoint = FieldVector(x=0.5, y=0.5, z=0.5) + duration = 10.0 # seconds + current_driver.ramp_simultaneously(setpoint=setpoint, duration=duration) + # After the ramp, the setpoint should be updated + assert np.allclose(current_driver.cartesian(), [0.5, 0.5, 0.5]) + + +def test_calculate_axes_ramp_rates_for() -> None: + """Test the static method calculate_axes_ramp_rates_for.""" + start = FieldVector(x=0.0, y=0.0, z=0.0) + setpoint = FieldVector(x=1.0, y=0.0, z=0.0) + duration = 10.0 + rates = AMIModel4303D.calculate_axes_ramp_rates_for(start, setpoint, duration) + assert len(rates) == 3 + assert np.isclose(rates[0], 0.1) # 1.0 / 10.0 + assert np.isclose(rates[1], 0.0) + assert np.isclose(rates[2], 0.0) + + +def test_calculate_vector_ramp_rate_from_duration() -> None: + """Test the static method calculate_vector_ramp_rate_from_duration.""" + start = FieldVector(x=0.0, y=0.0, z=0.0) + setpoint = FieldVector(x=3.0, y=4.0, z=0.0) + duration = 10.0 + rate = AMIModel4303D.calculate_vector_ramp_rate_from_duration( + start, setpoint, duration + ) + assert np.isclose(rate, 0.5) # distance=5.0, 5.0/10.0 = 0.5 + + +def test_raise_if_not_same_field_units(current_driver) -> None: + """Test that mismatched field_units raises ValueError.""" + current_driver._instrument_x.field_units("kilogauss") + with pytest.raises(ValueError, match="field_units"): + current_driver._raise_if_not_same_field_and_ramp_rate_units() + # Clean up + current_driver._instrument_x.field_units("tesla") + + +def test_raise_if_not_same_ramp_rate_units(current_driver) -> None: + """Test that mismatched ramp_rate_units raises ValueError.""" + current_driver._instrument_x.ramp_rate_units("minutes") + with pytest.raises(ValueError, match="ramp_rate_units"): + current_driver._raise_if_not_same_field_and_ramp_rate_units() + # Clean up + current_driver._instrument_x.ramp_rate_units("seconds") + + +def test_adjust_child_instruments_raises_when_axis_ramping(current_driver) -> None: + """Test that _adjust_child_instruments raises when an axis is ramping.""" + current_driver._instrument_x.write("CONF:STATE 1") # ramping + with pytest.raises(AMI430Exception, match="is already ramping"): + current_driver._adjust_child_instruments((0.5, 0.5, 0.5)) + # Clean up + current_driver._instrument_x.write("CONF:STATE 2") # holding + + +def test_update_individual_axes_ramp_rates_raises_without_vector_ramp_rate( + current_driver, +) -> None: + """Test that _update_individual_axes_ramp_rates raises when vector_ramp_rate is None.""" + # vector_ramp_rate is None by default (no initial_value set, get_cmd is None) + assert current_driver.vector_ramp_rate() is None + with pytest.raises(ValueError, match="vector_ramp_rate"): + current_driver._update_individual_axes_ramp_rates((0.5, 0.5, 0.5)) + + +def test_simultaneous_ramp_skips_axis_already_at_target( + current_driver, caplog: LogCaptureFixture +) -> None: + """Test that simultaneous ramp skips an axis that is already at its target.""" + # Set to a known state + current_driver.cartesian((0.5, 0.0, 0.0)) + + current_driver.ramp_mode("simultaneous") + current_driver.vector_ramp_rate(0.1) + + with caplog.at_level(logging.DEBUG, logger=LOG_NAME): + # Ramp to (0.5, 0.5, 0.0) - x is already at 0.5 + current_driver.cartesian((0.5, 0.5, 0.0)) + + messages = [record.message for record in caplog.records] + assert any("already at target field" in msg for msg in messages) + + # Clean up + current_driver.ramp_mode("default") + + +def test_3d_driver_pause(current_driver) -> None: + """Test that AMIModel4303D.pause pauses all axes without error.""" + current_driver.pause() From aa6d5d2cea1e365f85e5d728c1dcace0de180685 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 6 Apr 2026 11:46:34 +0200 Subject: [PATCH 10/11] Add types to ami430 tests --- tests/drivers/test_ami430_visa.py | 237 +++++++++++++++++++----------- 1 file changed, 152 insertions(+), 85 deletions(-) diff --git a/tests/drivers/test_ami430_visa.py b/tests/drivers/test_ami430_visa.py index 8b54dc891cc..cee046887be 100644 --- a/tests/drivers/test_ami430_visa.py +++ b/tests/drivers/test_ami430_visa.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import io import logging import re @@ -27,13 +29,15 @@ ) if TYPE_CHECKING: - from collections.abc import Callable + from collections.abc import Callable, Generator + + from pytest_mock import MockerFixture _time_resolution = time.get_clock_info("time").resolution # If any of the field limit functions are satisfied we are in the safe zone. # We can have higher field along the z-axis if x and y are zero. -field_limit: list["Callable[[float, float, float], bool]"] = [ +field_limit: list[Callable[[float, float, float], bool]] = [ lambda x, y, z: x == 0 and y == 0 and z < 3, lambda x, y, z: bool(np.linalg.norm([x, y, z]) < 2), ] @@ -42,7 +46,9 @@ @pytest.fixture(scope="function") -def magnet_axes_instances(): +def magnet_axes_instances() -> Generator[ + tuple[AMIModel430, AMIModel430, AMIModel430], None, None +]: """ Start three mock instruments representing current drivers for the x, y, and z directions. @@ -65,7 +71,9 @@ def magnet_axes_instances(): @pytest.fixture(name="current_driver", scope="function") -def _make_current_driver(magnet_axes_instances): +def _make_current_driver( + magnet_axes_instances: tuple[AMIModel430, AMIModel430, AMIModel430], +) -> Generator[AMIModel4303D, None, None]: """ Instantiate AMI430_3D instrument with the three mock instruments representing current drivers for the x, y, and z directions. @@ -80,7 +88,7 @@ def _make_current_driver(magnet_axes_instances): @pytest.fixture(scope="function", name="ami430") -def _make_ami430(): +def _make_ami430() -> Generator[AMIModel430, None, None]: mag = AMIModel430( "ami430", address="GPIB::1::INSTR", @@ -124,7 +132,8 @@ def _make_ami430(): def test_instantiation_from_names( - magnet_axes_instances, request: FixtureRequest + magnet_axes_instances: tuple[AMIModel430, AMIModel430, AMIModel430], + request: FixtureRequest, ) -> None: """ Instantiate AMI430_3D instrument from the three mock instruments @@ -141,7 +150,7 @@ def test_instantiation_from_names( assert driver._instrument_z is mag_z -def test_parent_instrument_is_set_on_child_axes(current_driver) -> None: +def test_parent_instrument_is_set_on_child_axes(current_driver: AMIModel4303D) -> None: """ Test that after creating AMIModel4303D, the _parent_instrument attribute on each child axis instrument is set to the 3D driver instance. @@ -151,7 +160,7 @@ def test_parent_instrument_is_set_on_child_axes(current_driver) -> None: assert current_driver._instrument_z._parent_instrument is current_driver -def test_parent_instrument_is_none_without_3d_driver(ami430) -> None: +def test_parent_instrument_is_none_without_3d_driver(ami430: AMIModel430) -> None: """ Test that a standalone AMIModel430 that is not part of a 3D driver has _parent_instrument set to None. @@ -159,7 +168,9 @@ def test_parent_instrument_is_none_without_3d_driver(ami430) -> None: assert ami430._parent_instrument is None -def test_request_field_change_via_child_set_field(current_driver) -> None: +def test_request_field_change_via_child_set_field( + current_driver: AMIModel4303D, +) -> None: """ Test that calling set_field on a child instrument that belongs to a 3D driver delegates to the 3D driver's _request_field_change, which routes @@ -179,7 +190,9 @@ def test_request_field_change_via_child_set_field(current_driver) -> None: assert np.isclose(current_driver.z(), 0.0) -def test_request_field_change_respects_field_limits(current_driver) -> None: +def test_request_field_change_respects_field_limits( + current_driver: AMIModel4303D, +) -> None: """ Test that calling set_field on a child instrument still respects the 3D driver's field limits when _request_field_change delegates @@ -195,7 +208,7 @@ def test_request_field_change_respects_field_limits(current_driver) -> None: current_driver._instrument_z.set_field(1.5) -def test_request_field_change_for_each_axis(current_driver) -> None: +def test_request_field_change_for_each_axis(current_driver: AMIModel4303D) -> None: """ Test that _request_field_change correctly routes for each axis (x, y, z) by setting fields on each child instrument individually. @@ -216,7 +229,7 @@ def test_request_field_change_for_each_axis(current_driver) -> None: def test_request_field_change_unknown_instrument_raises( - current_driver, request: FixtureRequest + current_driver: AMIModel4303D, request: FixtureRequest ) -> None: """ Test that _request_field_change raises a NameError when called with @@ -235,7 +248,7 @@ def test_request_field_change_unknown_instrument_raises( def test_child_set_field_bypasses_parent_when_safety_check_false( - current_driver, + current_driver: AMIModel4303D, ) -> None: """ Test that calling set_field with perform_safety_check=False on a child @@ -342,7 +355,8 @@ def test_sim_visa_reset_on_fully_closed(request: FixtureRequest) -> None: def test_instantiation_from_name_of_nonexistent_ami_instrument( - magnet_axes_instances, request: FixtureRequest + magnet_axes_instances: tuple[AMIModel430, AMIModel430, AMIModel430], + request: FixtureRequest, ) -> None: mag_x, mag_y, mag_z = magnet_axes_instances request.addfinalizer(Instrument.close_all) @@ -358,7 +372,8 @@ def test_instantiation_from_name_of_nonexistent_ami_instrument( def test_instantiation_from_name_of_existing_non_ami_instrument( - magnet_axes_instances, request: FixtureRequest + magnet_axes_instances: tuple[AMIModel430, AMIModel430, AMIModel430], + request: FixtureRequest, ) -> None: mag_x, _mag_y, mag_z = magnet_axes_instances request.addfinalizer(Instrument.close_all) @@ -383,7 +398,8 @@ def test_instantiation_from_name_of_existing_non_ami_instrument( def test_instantiation_from_badly_typed_argument( - magnet_axes_instances, request: FixtureRequest + magnet_axes_instances: tuple[AMIModel430, AMIModel430, AMIModel430], + request: FixtureRequest, ) -> None: mag_x, mag_y, _mag_z = magnet_axes_instances request.addfinalizer(Instrument.close_all) @@ -406,7 +422,9 @@ def test_instantiation_from_badly_typed_argument( suppress_health_check=(HealthCheck.function_scoped_fixture,), deadline=None, ) -def test_cartesian_sanity(current_driver, set_target) -> None: +def test_cartesian_sanity( + current_driver: AMIModel4303D, set_target: tuple[float, float, float] +) -> None: """ A sanity check to see if the driver remember vectors in any random configuration in cartesian coordinates @@ -429,7 +447,9 @@ def test_cartesian_sanity(current_driver, set_target) -> None: suppress_health_check=(HealthCheck.function_scoped_fixture,), deadline=None, ) -def test_spherical_sanity(current_driver, set_target) -> None: +def test_spherical_sanity( + current_driver: AMIModel4303D, set_target: tuple[float, float, float] +) -> None: """ A sanity check to see if the driver remember vectors in any random configuration in spherical coordinates @@ -452,7 +472,9 @@ def test_spherical_sanity(current_driver, set_target) -> None: suppress_health_check=(HealthCheck.function_scoped_fixture,), deadline=None, ) -def test_cylindrical_sanity(current_driver, set_target) -> None: +def test_cylindrical_sanity( + current_driver: AMIModel4303D, set_target: tuple[float, float, float] +) -> None: """ A sanity check to see if the driver remember vectors in any random configuration in cylindrical coordinates @@ -479,7 +501,9 @@ def test_cylindrical_sanity(current_driver, set_target) -> None: suppress_health_check=(HealthCheck.function_scoped_fixture,), deadline=None, ) -def test_cartesian_setpoints(current_driver, set_target) -> None: +def test_cartesian_setpoints( + current_driver: AMIModel4303D, set_target: tuple[float, float, float] +) -> None: """ 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 @@ -504,7 +528,9 @@ def test_cartesian_setpoints(current_driver, set_target) -> None: suppress_health_check=(HealthCheck.function_scoped_fixture,), deadline=None, ) -def test_spherical_setpoints(current_driver, set_target) -> None: +def test_spherical_setpoints( + current_driver: AMIModel4303D, set_target: tuple[float, float, float] +) -> None: """ 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 @@ -517,9 +543,9 @@ def test_spherical_setpoints(current_driver, set_target) -> None: phi = current_driver.phi() get_target = dict(zip(("r", "theta", "phi"), (r, theta, phi))) - set_target = dict(zip(("r", "theta", "phi"), set_target)) + set_target_dict: dict[str, float] = dict(zip(("r", "theta", "phi"), set_target)) - set_vector = FieldVector(**set_target) + set_vector = FieldVector(**set_target_dict) get_vector = FieldVector(**get_target) assert set_vector.is_equal(get_vector) @@ -534,7 +560,9 @@ def test_spherical_setpoints(current_driver, set_target) -> None: deadline=500, suppress_health_check=(HealthCheck.function_scoped_fixture,), ) -def test_cylindrical_setpoints(current_driver, set_target) -> None: +def test_cylindrical_setpoints( + current_driver: AMIModel4303D, set_target: tuple[float, float, float] +) -> None: """ 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 @@ -547,9 +575,9 @@ def test_cylindrical_setpoints(current_driver, set_target) -> None: phi = current_driver.phi() get_target = dict(zip(("rho", "phi", "z"), (rho, phi, z))) - set_target = dict(zip(("rho", "phi", "z"), set_target)) + set_target_dict: dict[str, float] = dict(zip(("rho", "phi", "z"), set_target)) - set_vector = FieldVector(**set_target) + set_vector = FieldVector(**set_target_dict) get_vector = FieldVector(**get_target) assert set_vector.is_equal(get_vector) @@ -560,7 +588,9 @@ def test_cylindrical_setpoints(current_driver, set_target) -> None: deadline=500, suppress_health_check=(HealthCheck.function_scoped_fixture,), ) -def test_measured(current_driver, set_target) -> None: +def test_measured( + current_driver: AMIModel4303D, set_target: tuple[float, float, float] +) -> None: """ Simply call the measurement methods and verify that no exceptions are raised. @@ -615,7 +645,9 @@ def get_ramp_down_order(messages: list[str]) -> list[str]: return order -def test_ramp_down_first(current_driver, caplog: LogCaptureFixture) -> None: +def test_ramp_down_first( + current_driver: AMIModel4303D, caplog: LogCaptureFixture +) -> None: """ To prevent quenching of the magnets, we need the driver to always be within the field limits. Part of the strategy of making sure @@ -651,7 +683,7 @@ def test_ramp_down_first(current_driver, caplog: LogCaptureFixture) -> None: assert order[0][0] == ramp_down_name -def test_field_limit_exception(current_driver) -> None: +def test_field_limit_exception(current_driver: AMIModel4303D) -> None: """ Test that an exception is raised if we intentionally set the field beyond the limits. Together with the no_test_ramp_down_first test @@ -681,7 +713,7 @@ def test_field_limit_exception(current_driver) -> None: assert belief -def test_cylindrical_poles(current_driver) -> None: +def test_cylindrical_poles(current_driver: AMIModel4303D) -> None: """ Test that the phi coordinate is remembered even if the resulting vector is equivalent to the null vector @@ -699,7 +731,7 @@ def test_cylindrical_poles(current_driver) -> None: assert np.allclose([rho_m, phi_m, z_m], [rho, phi, z]) -def test_spherical_poles(current_driver) -> None: +def test_spherical_poles(current_driver: AMIModel4303D) -> None: """ Test that the theta and phi coordinates are remembered even if the resulting vector is equivalent to the null vector @@ -717,7 +749,7 @@ def test_spherical_poles(current_driver) -> None: assert np.allclose([field_m, theta_m, phi_m], [field, theta, phi]) -def test_ramp_rate_exception(current_driver) -> None: +def test_ramp_rate_exception(current_driver: AMIModel4303D) -> None: """ Test that an exception is raised if we try to set the ramp rate to a higher value than is allowed @@ -731,7 +763,7 @@ def test_ramp_rate_exception(current_driver) -> None: def test_simultaneous_ramp_mode_does_not_reset_individual_axis_ramp_rates_if_nonblocking_ramp( - current_driver, caplog: LogCaptureFixture, request: FixtureRequest + current_driver: AMIModel4303D, caplog: LogCaptureFixture, request: FixtureRequest ) -> None: ami3d = current_driver @@ -823,7 +855,7 @@ def test_simultaneous_ramp_mode_does_not_reset_individual_axis_ramp_rates_if_non def test_simultaneous_ramp_mode_resets_individual_axis_ramp_rates_if_blocking_ramp( - current_driver, caplog: LogCaptureFixture, request: FixtureRequest + current_driver: AMIModel4303D, caplog: LogCaptureFixture, request: FixtureRequest ) -> None: ami3d = current_driver @@ -892,7 +924,9 @@ def test_simultaneous_ramp_mode_resets_individual_axis_ramp_rates_if_blocking_ra ) -def test_reducing_field_ramp_limit_reduces_a_higher_ramp_rate(ami430) -> None: +def test_reducing_field_ramp_limit_reduces_a_higher_ramp_rate( + ami430: AMIModel430, +) -> None: """ When reducing field_ramp_limit, the actual ramp_rate should also be reduced if the new field_ramp_limit is lower than the actual ramp_rate @@ -914,7 +948,9 @@ def test_reducing_field_ramp_limit_reduces_a_higher_ramp_rate(ami430) -> None: assert ami430.ramp_rate() == ami430.field_ramp_limit() -def test_reducing_current_ramp_limit_reduces_a_higher_ramp_rate(ami430) -> None: +def test_reducing_current_ramp_limit_reduces_a_higher_ramp_rate( + ami430: AMIModel430, +) -> None: """ When reducing current_ramp_limit, the actual ramp_rate should also be reduced if the new current_ramp_limit is lower than the actual ramp_rate @@ -937,7 +973,9 @@ def test_reducing_current_ramp_limit_reduces_a_higher_ramp_rate(ami430) -> None: assert ami430.ramp_rate() == ami430.field_ramp_limit() -def test_reducing_field_ramp_limit_keeps_a_lower_ramp_rate_as_is(ami430) -> None: +def test_reducing_field_ramp_limit_keeps_a_lower_ramp_rate_as_is( + ami430: AMIModel430, +) -> None: """ When reducing field_ramp_limit, the actual ramp_rate should remain if the new field_ramp_limit is higher than the actual ramp_rate now. @@ -960,7 +998,9 @@ def test_reducing_field_ramp_limit_keeps_a_lower_ramp_rate_as_is(ami430) -> None assert ami430.ramp_rate() == old_ramp_rate -def test_reducing_current_ramp_limit_keeps_a_lower_ramp_rate_as_is(ami430) -> None: +def test_reducing_current_ramp_limit_keeps_a_lower_ramp_rate_as_is( + ami430: AMIModel430, +) -> None: """ When reducing current_ramp_limit, the actual ramp_rate should remain if the new current_ramp_limit is higher than the actual ramp_rate now @@ -985,7 +1025,9 @@ def test_reducing_current_ramp_limit_keeps_a_lower_ramp_rate_as_is(ami430) -> No assert ami430.ramp_rate() == old_ramp_rate -def test_blocking_ramp_parameter(current_driver, caplog: LogCaptureFixture) -> None: +def test_blocking_ramp_parameter( + current_driver: AMIModel4303D, caplog: LogCaptureFixture +) -> None: assert current_driver.block_during_ramp() is True with caplog.at_level(logging.DEBUG, logger=LOG_NAME): @@ -1005,7 +1047,7 @@ def test_blocking_ramp_parameter(current_driver, caplog: LogCaptureFixture) -> N assert len([mssg for mssg in messages if "blocking" in mssg]) == 0 -def test_current_and_field_params_interlink_at_init(ami430) -> None: +def test_current_and_field_params_interlink_at_init(ami430: AMIModel430) -> None: """ Test that the values of the ``coil_constant``-dependent parameters are correctly proportional to each other at the initialization of the @@ -1023,7 +1065,7 @@ def test_current_and_field_params_interlink_at_init(ami430) -> None: def test_current_and_field_params_interlink__change_current_ramp_limit( - ami430, factor=0.9 + ami430: AMIModel430, factor: float = 0.9 ) -> None: """ Test that after changing ``current_ramp_limit``, the values of the @@ -1065,7 +1107,7 @@ def test_current_and_field_params_interlink__change_current_ramp_limit( def test_current_and_field_params_interlink__change_field_ramp_limit( - ami430, factor=0.9 + ami430: AMIModel430, factor: float = 0.9 ) -> None: """ Test that after changing ``field_ramp_limit``, the values of the @@ -1107,7 +1149,7 @@ def test_current_and_field_params_interlink__change_field_ramp_limit( def test_current_and_field_params_interlink__change_coil_constant( - ami430, factor: float = 3 + ami430: AMIModel430, factor: float = 3 ) -> None: """ Test that after changing ``change_coil_constant``, the values of the @@ -1149,7 +1191,9 @@ def test_current_and_field_params_interlink__change_coil_constant( np.testing.assert_almost_equal(field_limit, current_limit * coil_constant) -def test_current_and_field_params_interlink__permutations_of_tests(ami430) -> None: +def test_current_and_field_params_interlink__permutations_of_tests( + ami430: AMIModel430, +) -> None: """ As per one of the user's request, the test_current_and_field_params_interlink__* tests are executed here with @@ -1260,7 +1304,9 @@ def _parametrization_kwargs() -> PDict: @pytest.mark.parametrize("field_limit", **_parametrization_kwargs()) def test_numeric_field_limit( - magnet_axes_instances, field_limit, request: FixtureRequest + magnet_axes_instances: tuple[AMIModel430, AMIModel430, AMIModel430], + field_limit: Any, + request: FixtureRequest, ) -> None: mag_x, mag_y, mag_z = magnet_axes_instances ami = AMIModel4303D("AMI430_3D", mag_x, mag_y, mag_z, field_limit) @@ -1278,7 +1324,7 @@ def test_numeric_field_limit( ami.cartesian(target_outside_limit) -def test_ramp_rate_units_and_field_units_at_init(ami430) -> None: +def test_ramp_rate_units_and_field_units_at_init(ami430: AMIModel430) -> None: """ Test values of ramp_rate_units and field_units parameters at init, and the units of other parameters which depend on the @@ -1305,7 +1351,7 @@ def test_ramp_rate_units_and_field_units_at_init(ami430) -> None: ids=("seconds", "minutes"), ) def test_change_ramp_rate_units_parameter( - ami430, new_value, unit_string, scale + ami430: AMIModel430, new_value: str, unit_string: str, scale: float ) -> None: """ Test that changing value of ramp_rate_units parameter is reflected in @@ -1338,7 +1384,10 @@ def test_change_ramp_rate_units_parameter( assert ami430.current_ramp_limit.scale == scale # Assert `coil_constant` value has been updated - assert ami430.coil_constant.get_latest.get_timestamp() > coil_constant_timestamp + new_timestamp = ami430.coil_constant.get_latest.get_timestamp() + assert new_timestamp is not None + assert coil_constant_timestamp is not None + assert new_timestamp > coil_constant_timestamp ami430.ramp_rate_units("seconds") @@ -1348,7 +1397,9 @@ def test_change_ramp_rate_units_parameter( (("tesla", "T"), ("kilogauss", "kG")), ids=("tesla", "kilogauss"), ) -def test_change_field_units_parameter(ami430, new_value, unit_string) -> None: +def test_change_field_units_parameter( + ami430: AMIModel430, new_value: str, unit_string: str +) -> None: """ Test that changing value of field_units parameter is reflected in settings of other magnet parameters. @@ -1378,12 +1429,15 @@ def test_change_field_units_parameter(ami430, new_value, unit_string) -> None: assert ami430.field_ramp_limit.unit.startswith(unit_string + "/") # Assert `coil_constant` value has been updated - assert ami430.coil_constant.get_latest.get_timestamp() > coil_constant_timestamp + new_timestamp = ami430.coil_constant.get_latest.get_timestamp() + assert new_timestamp is not None + assert coil_constant_timestamp is not None + assert new_timestamp > coil_constant_timestamp ami430.field_units("tesla") -def test_switch_heater_enabled(ami430, caplog) -> None: +def test_switch_heater_enabled(ami430: AMIModel430, caplog: LogCaptureFixture) -> None: assert ami430.switch_heater.enabled() is False # make sure that getting snapshot with heater disabled works without warning caplog.clear() @@ -1396,7 +1450,7 @@ def test_switch_heater_enabled(ami430, caplog) -> None: assert ami430.switch_heater.enabled() is False -def test_get_error(ami430) -> None: +def test_get_error(ami430: AMIModel430) -> None: """ Test that get_error queries the instrument and returns a string response. """ @@ -1405,21 +1459,21 @@ def test_get_error(ami430) -> None: assert "No Error" in result -def test_ramp(ami430) -> None: +def test_ramp(ami430: AMIModel430) -> None: """ Test that the ramp method sends the RAMP command without raising. """ ami430.ramp() -def test_pause(ami430) -> None: +def test_pause(ami430: AMIModel430) -> None: """ Test that the pause method sends the PAUSE command without raising. """ ami430.pause() -def test_zero(ami430) -> None: +def test_zero(ami430: AMIModel430) -> None: """ Test that the zero method sends the ZERO command without raising and invalidates the field parameter cache. @@ -1434,7 +1488,7 @@ def test_zero(ami430) -> None: assert ami430.field.cache.valid is False -def test_zero_updates_parent_setpoint(current_driver) -> None: +def test_zero_updates_parent_setpoint(current_driver: AMIModel4303D) -> None: """ Test that calling zero() on a child instrument updates the parent 3D driver's internal _set_point so that setpoint tracking remains @@ -1452,7 +1506,7 @@ def test_zero_updates_parent_setpoint(current_driver) -> None: assert np.isclose(current_driver.z(), 0.7) -def test_zero_updates_parent_setpoint_each_axis(current_driver) -> None: +def test_zero_updates_parent_setpoint_each_axis(current_driver: AMIModel4303D) -> None: """ Test that calling zero() on each child axis individually updates the corresponding component of the parent 3D driver's _set_point. @@ -1473,7 +1527,7 @@ def test_zero_updates_parent_setpoint_each_axis(current_driver) -> None: assert np.allclose(current_driver.cartesian(), [0.0, 0.0, 0.0]) -def test_reset_quench(ami430) -> None: +def test_reset_quench(ami430: AMIModel430) -> None: """ Test that reset_quench clears the quench condition on the instrument. """ @@ -1483,7 +1537,7 @@ def test_reset_quench(ami430) -> None: assert ami430.is_quenched() is False -def test_set_quenched(ami430) -> None: +def test_set_quenched(ami430: AMIModel430) -> None: """ Test that set_quenched sets the quench condition on the instrument. """ @@ -1494,14 +1548,14 @@ def test_set_quenched(ami430) -> None: ami430.reset_quench() -def test_reset(ami430) -> None: +def test_reset(ami430: AMIModel430) -> None: """ Test that the reset method sends the *RST command without raising. """ ami430.reset() -def test_can_start_ramping_returns_false_when_quenched(ami430) -> None: +def test_can_start_ramping_returns_false_when_quenched(ami430: AMIModel430) -> None: """ Test that _can_start_ramping returns False when the instrument is in a quench condition, and that set_field raises accordingly. @@ -1517,7 +1571,7 @@ def test_can_start_ramping_returns_false_when_quenched(ami430) -> None: ami430.reset_quench() -def test_can_start_ramping_returns_false_when_persistent(ami430) -> None: +def test_can_start_ramping_returns_false_when_persistent(ami430: AMIModel430) -> None: """ Test that _can_start_ramping returns False when the instrument is in persistent mode, and that set_field raises accordingly. @@ -1534,7 +1588,9 @@ def test_can_start_ramping_returns_false_when_persistent(ami430) -> None: ami430.write("CONF:PERS 0") -def test_can_start_ramping_returns_false_in_unexpected_state(ami430) -> None: +def test_can_start_ramping_returns_false_in_unexpected_state( + ami430: AMIModel430, +) -> None: """ Test that _can_start_ramping returns False when the instrument is in a state that is not 'holding', 'paused', 'at zero current', or @@ -1554,7 +1610,7 @@ def test_can_start_ramping_returns_false_in_unexpected_state(ami430) -> None: ami430.write("CONF:STATE 2") -def test_can_start_ramping_returns_true_when_holding(ami430) -> None: +def test_can_start_ramping_returns_true_when_holding(ami430: AMIModel430) -> None: """ Test that _can_start_ramping returns True when the instrument is in the 'holding' state (the default simulator state). @@ -1563,7 +1619,7 @@ def test_can_start_ramping_returns_true_when_holding(ami430) -> None: assert ami430._can_start_ramping() is True -def test_can_start_ramping_returns_true_when_paused(ami430) -> None: +def test_can_start_ramping_returns_true_when_paused(ami430: AMIModel430) -> None: """ Test that _can_start_ramping returns True when the instrument is in the 'paused' state. @@ -1576,7 +1632,9 @@ def test_can_start_ramping_returns_true_when_paused(ami430) -> None: ami430.write("CONF:STATE 2") -def test_can_start_ramping_returns_true_when_at_zero_current(ami430) -> None: +def test_can_start_ramping_returns_true_when_at_zero_current( + ami430: AMIModel430, +) -> None: """ Test that _can_start_ramping returns True when the instrument is in the 'at zero current' state. @@ -1589,7 +1647,9 @@ def test_can_start_ramping_returns_true_when_at_zero_current(ami430) -> None: ami430.write("CONF:STATE 2") -def test_can_start_ramping_when_ramping_with_heater_disabled(ami430) -> None: +def test_can_start_ramping_when_ramping_with_heater_disabled( + ami430: AMIModel430, +) -> None: """ Test that _can_start_ramping returns True when already ramping and the switch heater is not enabled. @@ -1603,7 +1663,9 @@ def test_can_start_ramping_when_ramping_with_heater_disabled(ami430) -> None: ami430.write("CONF:STATE 2") -def test_can_start_ramping_when_ramping_with_heater_enabled_and_on(ami430) -> None: +def test_can_start_ramping_when_ramping_with_heater_enabled_and_on( + ami430: AMIModel430, +) -> None: """ Test that _can_start_ramping returns True when already ramping and the switch heater is enabled and on (warm). @@ -1624,7 +1686,7 @@ def test_can_start_ramping_when_ramping_with_heater_enabled_and_on(ami430) -> No def test_can_start_ramping_returns_false_when_ramping_with_heater_enabled_and_off( - ami430, + ami430: AMIModel430, ) -> None: """ Test that _can_start_ramping returns False when already ramping and @@ -1644,7 +1706,7 @@ def test_can_start_ramping_returns_false_when_ramping_with_heater_enabled_and_of ami430.write("CONF:STATE 2") -def test_switch_heater_on_raises_when_not_enabled(ami430) -> None: +def test_switch_heater_on_raises_when_not_enabled(ami430: AMIModel430) -> None: """ Test that turning the switch heater on when it is not enabled raises an AMI430Exception. @@ -1654,7 +1716,7 @@ def test_switch_heater_on_raises_when_not_enabled(ami430) -> None: ami430.switch_heater.state(True) -def test_switch_heater_off_raises_when_not_enabled(ami430) -> None: +def test_switch_heater_off_raises_when_not_enabled(ami430: AMIModel430) -> None: """ Test that turning the switch heater off when it is not enabled raises an AMI430Exception. @@ -1664,7 +1726,7 @@ def test_switch_heater_off_raises_when_not_enabled(ami430) -> None: ami430.switch_heater.state(False) -def test_switch_heater_on_when_enabled(ami430) -> None: +def test_switch_heater_on_when_enabled(ami430: AMIModel430) -> None: """ Test that setting switch heater state to True succeeds when the switch heater is enabled. @@ -1677,7 +1739,7 @@ def test_switch_heater_on_when_enabled(ami430) -> None: ami430.switch_heater.enabled(False) -def test_switch_heater_off_when_enabled(ami430) -> None: +def test_switch_heater_off_when_enabled(ami430: AMIModel430) -> None: """ Test that setting switch heater state to False succeeds when the switch heater is enabled. @@ -1689,7 +1751,7 @@ def test_switch_heater_off_when_enabled(ami430) -> None: ami430.switch_heater.enabled(False) -def test_ami430_init_with_reset(request: FixtureRequest, mocker) -> None: +def test_ami430_init_with_reset(request: FixtureRequest, mocker: MockerFixture) -> None: """ Test that AMIModel430 can be instantiated with reset=True to cover the reset path during initialization. @@ -1731,7 +1793,7 @@ def test_ami430_init_with_custom_current_ramp_limit( assert mag.current_ramp_limit() == custom_limit -def test_set_field_exceeding_field_limit(ami430) -> None: +def test_set_field_exceeding_field_limit(ami430: AMIModel430) -> None: """ Test that set_field raises a ValueError when the requested field exceeds the individual instrument's field limit (coil_constant * @@ -1742,7 +1804,9 @@ def test_set_field_exceeding_field_limit(ami430) -> None: ami430.set_field(field_lim + 1, perform_safety_check=False) -def test_set_field_raises_when_switch_heater_enabled_but_off(ami430) -> None: +def test_set_field_raises_when_switch_heater_enabled_but_off( + ami430: AMIModel430, +) -> None: """ Test that set_field raises an AMI430Exception when the switch heater is enabled but its state is off (cold), meaning the persistent switch @@ -1757,7 +1821,7 @@ def test_set_field_raises_when_switch_heater_enabled_but_off(ami430) -> None: ami430.switch_heater.enabled(False) -def test_3d_driver_get_idn(current_driver) -> None: +def test_3d_driver_get_idn(current_driver: AMIModel4303D) -> None: """Test that AMIModel4303D.get_idn returns the expected IDN dict.""" idn = current_driver.get_idn() assert idn["vendor"] == "American Magnetics" @@ -1767,7 +1831,8 @@ def test_3d_driver_get_idn(current_driver) -> None: def test_3d_driver_invalid_field_limit_type( - magnet_axes_instances, request: FixtureRequest + magnet_axes_instances: tuple[AMIModel430, AMIModel430, AMIModel430], + request: FixtureRequest, ) -> None: """Test that passing an invalid field_limit type raises ValueError.""" mag_x, mag_y, mag_z = magnet_axes_instances @@ -1776,7 +1841,7 @@ def test_3d_driver_invalid_field_limit_type( AMIModel4303D("AMI430_3D", mag_x, mag_y, mag_z, None) # type: ignore[arg-type] -def test_ramp_simultaneously(current_driver) -> None: +def test_ramp_simultaneously(current_driver: AMIModel4303D) -> None: """Test the ramp_simultaneously method on AMIModel4303D.""" current_driver.cartesian((0.0, 0.0, 0.0)) setpoint = FieldVector(x=0.5, y=0.5, z=0.5) @@ -1809,7 +1874,7 @@ def test_calculate_vector_ramp_rate_from_duration() -> None: assert np.isclose(rate, 0.5) # distance=5.0, 5.0/10.0 = 0.5 -def test_raise_if_not_same_field_units(current_driver) -> None: +def test_raise_if_not_same_field_units(current_driver: AMIModel4303D) -> None: """Test that mismatched field_units raises ValueError.""" current_driver._instrument_x.field_units("kilogauss") with pytest.raises(ValueError, match="field_units"): @@ -1818,7 +1883,7 @@ def test_raise_if_not_same_field_units(current_driver) -> None: current_driver._instrument_x.field_units("tesla") -def test_raise_if_not_same_ramp_rate_units(current_driver) -> None: +def test_raise_if_not_same_ramp_rate_units(current_driver: AMIModel4303D) -> None: """Test that mismatched ramp_rate_units raises ValueError.""" current_driver._instrument_x.ramp_rate_units("minutes") with pytest.raises(ValueError, match="ramp_rate_units"): @@ -1827,7 +1892,9 @@ def test_raise_if_not_same_ramp_rate_units(current_driver) -> None: current_driver._instrument_x.ramp_rate_units("seconds") -def test_adjust_child_instruments_raises_when_axis_ramping(current_driver) -> None: +def test_adjust_child_instruments_raises_when_axis_ramping( + current_driver: AMIModel4303D, +) -> None: """Test that _adjust_child_instruments raises when an axis is ramping.""" current_driver._instrument_x.write("CONF:STATE 1") # ramping with pytest.raises(AMI430Exception, match="is already ramping"): @@ -1837,7 +1904,7 @@ def test_adjust_child_instruments_raises_when_axis_ramping(current_driver) -> No def test_update_individual_axes_ramp_rates_raises_without_vector_ramp_rate( - current_driver, + current_driver: AMIModel4303D, ) -> None: """Test that _update_individual_axes_ramp_rates raises when vector_ramp_rate is None.""" # vector_ramp_rate is None by default (no initial_value set, get_cmd is None) @@ -1847,7 +1914,7 @@ def test_update_individual_axes_ramp_rates_raises_without_vector_ramp_rate( def test_simultaneous_ramp_skips_axis_already_at_target( - current_driver, caplog: LogCaptureFixture + current_driver: AMIModel4303D, caplog: LogCaptureFixture ) -> None: """Test that simultaneous ramp skips an axis that is already at its target.""" # Set to a known state @@ -1867,6 +1934,6 @@ def test_simultaneous_ramp_skips_axis_already_at_target( current_driver.ramp_mode("default") -def test_3d_driver_pause(current_driver) -> None: +def test_3d_driver_pause(current_driver: AMIModel4303D) -> None: """Test that AMIModel4303D.pause pauses all axes without error.""" current_driver.pause() From 867b2d9b3eb7ec1530fa2ac177c7f502181a3607 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 6 Apr 2026 15:25:16 +0200 Subject: [PATCH 11/11] test that changing field via 3d inst wrapper really chanes field --- tests/drivers/test_ami430_visa.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/drivers/test_ami430_visa.py b/tests/drivers/test_ami430_visa.py index cee046887be..732e6dc4103 100644 --- a/tests/drivers/test_ami430_visa.py +++ b/tests/drivers/test_ami430_visa.py @@ -217,12 +217,15 @@ def test_request_field_change_for_each_axis(current_driver: AMIModel4303D) -> No current_driver._instrument_x.set_field(0.3) assert np.isclose(current_driver.x(), 0.3) + assert np.isclose(current_driver._instrument_x.field(), 0.3) current_driver._instrument_y.set_field(0.4) assert np.isclose(current_driver.y(), 0.4) + assert np.isclose(current_driver._instrument_y.field(), 0.4) current_driver._instrument_z.set_field(0.5) assert np.isclose(current_driver.z(), 0.5) + assert np.isclose(current_driver._instrument_z.field(), 0.5) # All three should now reflect the individually-set values assert np.allclose(current_driver.cartesian(), [0.3, 0.4, 0.5])