From 67c3110f12de23a91b9f29324bf3e3f7603e3db7 Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Wed, 29 Oct 2025 17:17:02 -0700 Subject: [PATCH 01/19] Add missing commands to pyvisa-sim file and rename properties for clarity --- .../instrument/sims/lakeshore_model336.yaml | 183 +++++++++++++++--- 1 file changed, 151 insertions(+), 32 deletions(-) diff --git a/src/qcodes/instrument/sims/lakeshore_model336.yaml b/src/qcodes/instrument/sims/lakeshore_model336.yaml index 1e0277c31b16..7da9c06b1c9e 100644 --- a/src/qcodes/instrument/sims/lakeshore_model336.yaml +++ b/src/qcodes/instrument/sims/lakeshore_model336.yaml @@ -37,21 +37,21 @@ devices: setter: q: "INNAME A,\"{}\"" - sensor_setpoint_A: - default: "100" + sensor_tlimit_A: + default: 0 getter: - q: "setp? A" + q: "TLIMIT? A" r: "{}" setter: - q: "setp A,\"{}\"" + q: "TLIMIT A,{}" - sensor_range_A: - default: "1" + sensor_intype_A: + default: "0,0,1,0,1" getter: - q: "range? A" + q: "INTYPE? A" r: "{}" setter: - q: "range A,\"{}\"" + q: "INTYPE A,{},{},{},{},{}" sensor_curve_number_A: default: 42 @@ -91,21 +91,21 @@ devices: setter: q: "INNAME B,\"{}\"" - sensor_setpoint_B: - default: "100" + sensor_tlimit_B: + default: 0 getter: - q: "setp? A" + q: "TLIMIT? B" r: "{}" setter: - q: "setp A,\"{}\"" + q: "TLIMIT B,{}" - sensor_range_B: - default: "1" + sensor_intype_B: + default: "0,0,1,0,1" getter: - q: "range? A" + q: "INTYPE? B" r: "{}" setter: - q: "range A,\"{}\"" + q: "INTYPE B,{},{},{},{},{}" sensor_curve_number_B: default: 41 @@ -144,21 +144,21 @@ devices: setter: q: "INNAME C,\"{}\"" - sensor_setpoint_C: - default: "100" + sensor_tlimit_C: + default: 0 getter: - q: "setp? A" + q: "TLIMIT? C" r: "{}" setter: - q: "setp A,\"{}\"" + q: "TLIMIT C,{}" - sensor_range_C: - default: "1" + sensor_intype_C: + default: "0,0,1,0,1" getter: - q: "range? A" + q: "INTYPE? C" r: "{}" setter: - q: "range A,\"{}\"" + q: "INTYPE C,{},{},{},{},{}" sensor_curve_number_C: default: 40 @@ -197,21 +197,21 @@ devices: setter: q: "INNAME D,\"{}\"" - sensor_setpoint_D: - default: "100" + sensor_tlimit_D: + default: 0 getter: - q: "setp? A" + q: "TLIMIT? D" r: "{}" setter: - q: "setp A,\"{}\"" + q: "TLIMIT D,{}" - sensor_range_D: - default: "1" + sensor_intype_D: + default: "0,0,1,0,1" getter: - q: "range? A" + q: "INTYPE? D" r: "{}" setter: - q: "range A,\"{}\"" + q: "INTYPE D,{},{},{},{},{}" sensor_curve_number_D: default: 39 @@ -224,6 +224,125 @@ devices: q: "CRVHDR? 39" r: "DT-039,01110039,2,339.0,1" + pid_output_1: + default: "80,20,0" + getter: + q: "PID? 1" + r: "{}" + setter: + q: "PID 1,{},{},{}" + + pid_output_2: + default: "80,20,0" + getter: + q: "PID? 2" + r: "{}" + setter: + q: "PID 2,{},{},{}" + + outmode_output_1: + default: "3,1,1" + getter: + q: "OUTMODE? 1" + r: "{}" + setter: + q: "OUTMODE 1,{},{},{}" + + outmode_output_2: + default: "3,1,1" + getter: + q: "OUTMODE? 2" + r: "{}" + setter: + q: "OUTMODE 2,{},{},{}" + + range_output_1: + default: 1 + getter: + q: "RANGE? 1" + r: "{}" + setter: + q: "RANGE 1,{}" + + range_output_2: + default: 1 + getter: + q: "RANGE? 2" + r: "{}" + setter: + q: "RANGE 2,{}" + + setpoint_output_1: + default: 0 + getter: + q: "SETP? 1" + r: "{}" + setter: + q: "SETP 1,{}" + + setpoint_output_2: + default: 0 + getter: + q: "SETP? 2" + r: "{}" + setter: + q: "SETP 2,{}" + + htr_output_1: + default: 0.005 + getter: + q: "HTR? 1" + r: "{}" + + htr_output_2: + default: 0.005 + getter: + q: "HTR? 2" + r: "{}" + + htrset_output_1: + default: "0,1,0,0,1" + getter: + q: "HTRSET? 1" + r: "{}" + setter: + q: "HTRSET 1,{},{},{},{},{}" + + htrset_output_2: + default: "0,1,0,0,1" + getter: + q: "HTRSET? 2" + r: "{}" + setter: + q: "HTRSET 2,{},{},{},{},{}" + + ramp_output_1: + default: "0,0" + getter: + q: "RAMP? 1" + r: "{}" + setter: + q: "RAMP 1,{},{}" + + ramp_output_2: + default: "0,0" + getter: + q: "RAMP? 2" + r: "{}" + setter: + q: "RAMP 2,{},{}" + + rampst_output_1: + default: 0 + getter: + q: "RAMPST? 1" + r: "{}" + + rampst_output_2: + default: 0 + getter: + q: "RAMPST? 2" + r: "{}" resources: GPIB::2::INSTR: From 02e02ce7da3c0487a029f84b8273fb2427d30a64 Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Wed, 29 Oct 2025 17:21:00 -0700 Subject: [PATCH 02/19] Bypass blocking function in blocking_t if in simulated mode --- .../instrument_drivers/Lakeshore/lakeshore_base.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py b/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py index 3305230337d8..a36dcbc7483b 100644 --- a/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py +++ b/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py @@ -373,6 +373,11 @@ def __init__( be reached within the current range. """ + @property + def _is_simulated(self) -> bool: + """Check if this instrument is using PyVISA simulation backend.""" + return getattr(self.root_instrument, "visabackend", None) == "sim" + def _set_blocking_t(self, temperature: float) -> None: self.set_range_from_temperature(temperature) self.setpoint(temperature) @@ -490,6 +495,10 @@ def wait_until_set_point_reached( t_setpoint = self.setpoint() + if self._is_simulated: + # In sim mode, bypass the wait loop by setting temperature to setpoint + active_channel.temperature(t_setpoint) + time_now = time.perf_counter() time_enter_tolerance_zone = time_now From 3544760c4e15c606102004a040b9e533814f9223 Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Fri, 31 Oct 2025 10:27:58 -0700 Subject: [PATCH 03/19] Fix issues with pyvisa-sim yaml setters/getters --- .../instrument/sims/lakeshore_model336.yaml | 138 +++++++++++++++--- 1 file changed, 120 insertions(+), 18 deletions(-) diff --git a/src/qcodes/instrument/sims/lakeshore_model336.yaml b/src/qcodes/instrument/sims/lakeshore_model336.yaml index 7da9c06b1c9e..dd77fbf24305 100644 --- a/src/qcodes/instrument/sims/lakeshore_model336.yaml +++ b/src/qcodes/instrument/sims/lakeshore_model336.yaml @@ -16,6 +16,8 @@ devices: getter: q: "KRDG? A" r: "{}" + setter: + q: "SIMTEMP A,{}" # Custom simulation-only command sensor_raw_A: default: 101.0 @@ -51,7 +53,7 @@ devices: q: "INTYPE? A" r: "{}" setter: - q: "INTYPE A,{},{},{},{},{}" + q: "INTYPE A,{}" sensor_curve_number_A: default: 42 @@ -70,6 +72,8 @@ devices: getter: q: "KRDG? B" r: "{}" + setter: + q: "SIMTEMP B,{}" # Custom simulation-only command sensor_raw_B: default: 101.0 @@ -105,7 +109,7 @@ devices: q: "INTYPE? B" r: "{}" setter: - q: "INTYPE B,{},{},{},{},{}" + q: "INTYPE B,{}" sensor_curve_number_B: default: 41 @@ -123,6 +127,8 @@ devices: getter: q: "KRDG? C" r: "{}" + setter: + q: "SIMTEMP C,{}" # Custom simulation-only command sensor_raw_C: default: 101.0 @@ -158,7 +164,7 @@ devices: q: "INTYPE? C" r: "{}" setter: - q: "INTYPE C,{},{},{},{},{}" + q: "INTYPE C,{}" sensor_curve_number_C: default: 40 @@ -176,6 +182,8 @@ devices: getter: q: "KRDG? D" r: "{}" + setter: + q: "SIMTEMP D,{}" # Custom simulation-only command sensor_raw_D: default: 101.0 @@ -211,7 +219,7 @@ devices: q: "INTYPE? D" r: "{}" setter: - q: "INTYPE D,{},{},{},{},{}" + q: "INTYPE D,{}" sensor_curve_number_D: default: 39 @@ -225,36 +233,36 @@ devices: r: "DT-039,01110039,2,339.0,1" pid_output_1: - default: "80,20,0" + default: "10,20,30" getter: q: "PID? 1" r: "{}" setter: - q: "PID 1,{},{},{}" + q: "PID 1,{}" pid_output_2: - default: "80,20,0" + default: "10,20,30" getter: q: "PID? 2" r: "{}" setter: - q: "PID 2,{},{},{}" + q: "PID 2,{}" outmode_output_1: - default: "3,1,1" + default: "1,2,0" getter: q: "OUTMODE? 1" r: "{}" setter: - q: "OUTMODE 1,{},{},{}" + q: "OUTMODE 1, {}" outmode_output_2: - default: "3,1,1" + default: "1,1,0" getter: q: "OUTMODE? 2" r: "{}" setter: - q: "OUTMODE 2,{},{},{}" + q: "OUTMODE 2, {}" range_output_1: default: 1 @@ -301,20 +309,20 @@ devices: r: "{}" htrset_output_1: - default: "0,1,0,0,1" + default: "1, 5" getter: q: "HTRSET? 1" r: "{}" setter: - q: "HTRSET 1,{},{},{},{},{}" + q: "HTRSET 1, {}" htrset_output_2: - default: "0,1,0,0,1" + default: "1, 5" getter: q: "HTRSET? 2" r: "{}" setter: - q: "HTRSET 2,{},{},{},{},{}" + q: "HTRSET 2, {}" ramp_output_1: default: "0,0" @@ -322,7 +330,7 @@ devices: q: "RAMP? 1" r: "{}" setter: - q: "RAMP 1,{},{}" + q: "RAMP 1,{}" ramp_output_2: default: "0,0" @@ -330,7 +338,7 @@ devices: q: "RAMP? 2" r: "{}" setter: - q: "RAMP 2,{},{}" + q: "RAMP 2,{}" rampst_output_1: default: 0 @@ -344,6 +352,100 @@ devices: q: "RAMPST? 2" r: "{}" + # ==================== + # Output 3 (Voltage Source, no PID) + # ==================== + outmode_output_3: + default: "1,1,0" + getter: + q: "OUTMODE? 3" + r: "{}" + setter: + q: "OUTMODE 3, {}" + + range_output_3: + default: 1 + getter: + q: "RANGE? 3" + r: "{}" + setter: + q: "RANGE 3,{}" + + setpoint_output_3: + default: 0 + getter: + q: "SETP? 3" + r: "{}" + setter: + q: "SETP 3,{}" + + htr_output_3: + default: 0 + getter: + q: "HTR? 3" + r: "{}" + + ramp_output_3: + default: "0,0" + getter: + q: "RAMP? 3" + r: "{}" + setter: + q: "RAMP 3,{}" + + rampst_output_3: + default: 0 + getter: + q: "RAMPST? 3" + r: "{}" + + # ==================== + # Output 4 (Voltage Source, no PID) + # ==================== + outmode_output_4: + default: "1,2,0" + getter: + q: "OUTMODE? 4" + r: "{}" + setter: + q: "OUTMODE 4, {}" + + range_output_4: + default: 1 + getter: + q: "RANGE? 4" + r: "{}" + setter: + q: "RANGE 4,{}" + + setpoint_output_4: + default: 0 + getter: + q: "SETP? 4" + r: "{}" + setter: + q: "SETP 4,{}" + + htr_output_4: + default: 0 + getter: + q: "HTR? 4" + r: "{}" + + ramp_output_4: + default: "0,0" + getter: + q: "RAMP? 4" + r: "{}" + setter: + q: "RAMP 4,{}" + + rampst_output_4: + default: 0 + getter: + q: "RAMPST? 4" + r: "{}" + resources: GPIB::2::INSTR: device: device 1 From a8960fa7e151b48a205a3b46aef12b79e519557b Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Fri, 31 Oct 2025 10:32:02 -0700 Subject: [PATCH 04/19] Update logic to bypass wait loop when setting blocking_t in sim mode. --- .../instrument_drivers/Lakeshore/lakeshore_base.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py b/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py index a36dcbc7483b..d3a046565bab 100644 --- a/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py +++ b/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py @@ -493,11 +493,12 @@ def wait_until_set_point_reached( f"be set to 'kelvin'." ) - t_setpoint = self.setpoint() - if self._is_simulated: - # In sim mode, bypass the wait loop by setting temperature to setpoint - active_channel.temperature(t_setpoint) + # Use custom SIMTEMP command to update read-only temperature sensor + # in order to "trick" wait loop into thinking temperature was ramped + self.write(f"SIMTEMP {active_channel_name_on_instrument},{self.setpoint()}") + + t_setpoint = self.setpoint() time_now = time.perf_counter() time_enter_tolerance_zone = time_now From 7b8b98a0ca7d0deee618e95ec7f7f5f845756bdd Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Fri, 31 Oct 2025 10:37:07 -0700 Subject: [PATCH 05/19] Fix bug where channel name was not being parsed correctly. --- .../instrument_drivers/Lakeshore/Lakeshore_model_336.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/qcodes/instrument_drivers/Lakeshore/Lakeshore_model_336.py b/src/qcodes/instrument_drivers/Lakeshore/Lakeshore_model_336.py index 977e04adec0f..509c087608e9 100644 --- a/src/qcodes/instrument_drivers/Lakeshore/Lakeshore_model_336.py +++ b/src/qcodes/instrument_drivers/Lakeshore/Lakeshore_model_336.py @@ -85,6 +85,10 @@ class LakeshoreModel336VoltageSource(LakeshoreBaseOutput): RANGES: ClassVar[dict[str, int]] = {"off": 0, "low": 1, "medium": 2, "high": 3} + _input_channel_parameter_kwargs: ClassVar[dict[str, dict[str, int]]] = { + "val_mapping": _channel_name_to_outmode_command_map + } + def __init__( self, parent: "LakeshoreModel336", From 461ad88ac9a6a5260005a02b5b49a650c120cc99 Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Fri, 31 Oct 2025 10:42:59 -0700 Subject: [PATCH 06/19] Use pyvisa-sim yaml for testing instead of mocked class --- tests/drivers/test_lakeshore_336.py | 235 +++------------------------- 1 file changed, 26 insertions(+), 209 deletions(-) diff --git a/tests/drivers/test_lakeshore_336.py b/tests/drivers/test_lakeshore_336.py index 0ba20e47b295..d9985d963ec0 100644 --- a/tests/drivers/test_lakeshore_336.py +++ b/tests/drivers/test_lakeshore_336.py @@ -1,208 +1,21 @@ -import logging -import time +import pytest -from qcodes.instrument import InstrumentBase from qcodes.instrument_drivers.Lakeshore import LakeshoreModel336 -from .test_lakeshore_372 import ( - DictClass, - MockVisaInstrument, - command, - instrument_fixture, - query, - split_args, -) -log = logging.getLogger(__name__) - -VISA_LOGGER = ".".join((InstrumentBase.__module__, "com", "visa")) - - -class LakeshoreModel336Mock(MockVisaInstrument, LakeshoreModel336): - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - - # initial values - self.heaters: dict[str, DictClass] = {} - self.heaters["1"] = DictClass( - P=1, - I=2, - D=3, - mode=1, # 'off' - input_channel=1, # 'A' - powerup_enable=0, - polarity=0, - use_filter=0, - delay=1, - output_range=0, - setpoint=4, - ) - self.heaters["2"] = DictClass( - P=1, - I=2, - D=3, - mode=2, # 'closed_loop' - input_channel=2, # 'B' - powerup_enable=0, - polarity=0, - use_filter=0, - delay=1, - output_range=0, - setpoint=4, - ) - self.heaters["3"] = DictClass( - mode=4, # 'monitor_out' - input_channel=2, # 'B' - powerup_enable=0, - polarity=0, - use_filter=0, - delay=1, - output_range=0, - setpoint=4, - ) - self.heaters["4"] = DictClass( - mode=5, # 'warm_up' - input_channel=1, # 'A' - powerup_enable=0, - polarity=0, - use_filter=0, - delay=1, - output_range=0, - setpoint=4, - ) - - self.channel_mock = { - str(i): DictClass( - t_limit=i, - T=4, - sensor_name=f"sensor_{i}", - sensor_type=1, # 'diode', - auto_range_enabled=0, # 'off', - range=0, - compensation_enabled=0, # False, - units=1, # 'kelvin' - ) - for i in self.channel_name_command.keys() - } - - # simulate delayed heating - self.simulate_heating = False - self.start_heating_time = time.perf_counter() - - def start_heating(self): - self.start_heating_time = time.perf_counter() - self.simulate_heating = True - - def get_t_when_heating(self): - """ - Simply define a fixed setpoint of 4 k for now - """ - delta = abs(time.perf_counter() - self.start_heating_time) - # make it simple to start with: linear ramp 1K per second - # start at 7K. - return max(4, 7 - delta) - - @query("PID?") - def pidq(self, arg): - heater = self.heaters[arg] - return f"{heater.P},{heater.I},{heater.D}" - - @command("PID") - @split_args() - def pid(self, output, P, I, D): # noqa E741 - for a, v in zip(["P", "I", "D"], [P, I, D]): - setattr(self.heaters[output], a, v) - - @query("OUTMODE?") - def outmodeq(self, arg): - heater = self.heaters[arg] - return f"{heater.mode},{heater.input_channel},{heater.powerup_enable}" - - @command("OUTMODE") - @split_args() - def outputmode(self, output, mode, input_channel, powerup_enable): - h = self.heaters[output] - h.output = output - h.mode = mode - h.input_channel = input_channel - h.powerup_enable = powerup_enable - - @query("INTYPE?") - def intypeq(self, channel): - ch = self.channel_mock[channel] - return ( - f"{ch.sensor_type}," - f"{ch.auto_range_enabled},{ch.range}," - f"{ch.compensation_enabled},{ch.units}" - ) - - @command("INTYPE") - @split_args() - def intype( - self, - channel, - sensor_type, - auto_range_enabled, - range_, - compensation_enabled, - units, - ): - ch = self.channel_mock[channel] - ch.sensor_type = sensor_type - ch.auto_range_enabled = auto_range_enabled - ch.range = range_ - ch.compensation_enabled = compensation_enabled - ch.units = units - - @query("RANGE?") - def rangeq(self, heater): - h = self.heaters[heater] - return f"{h.output_range}" - - @command("RANGE") - @split_args() - def range_cmd(self, heater, output_range): - h = self.heaters[heater] - h.output_range = output_range - - @query("SETP?") - def setpointq(self, heater): - h = self.heaters[heater] - return f"{h.setpoint}" - - @command("SETP") - @split_args() - def setpoint(self, heater, setpoint): - h = self.heaters[heater] - h.setpoint = setpoint - - @query("TLIMIT?") - def tlimitq(self, channel): - chan = self.channel_mock[channel] - return f"{chan.tlimit}" - - @command("TLIMIT") - @split_args() - def tlimitcmd(self, channel, tlimit): - chan = self.channel_mock[channel] - chan.tlimit = tlimit - - @query("KRDG?") - def temperature(self, output): - chan = self.channel_mock[output] - if self.simulate_heating: - return self.get_t_when_heating() - return f"{chan.T}" - - -@instrument_fixture(scope="function", name="lakeshore_336") +@pytest.fixture(scope="function", name="lakeshore_336") def _make_lakeshore_336(): - return LakeshoreModel336Mock( - "lakeshore_336_fixture", + """Create a Lakeshore 336 instance using PyVISA-sim backend.""" + inst = LakeshoreModel336( + "lakeshore_336", "GPIB::2::INSTR", pyvisa_sim_file="lakeshore_model336.yaml", device_clear=False, ) + try: + yield inst + finally: + inst.close() def test_pid_set(lakeshore_336) -> None: @@ -218,19 +31,19 @@ def test_pid_set(lakeshore_336) -> None: assert (h.P(), h.I(), h.D()) == (P, I, D) -def test_output_mode(lakeshore_336) -> None: +@pytest.mark.parametrize("output_num", [1, 2, 3, 4]) +@pytest.mark.parametrize("mode", ["off", "closed_loop", "zone", "open_loop"]) +@pytest.mark.parametrize("input_channel", ["A", "B", "C", "D"]) +def test_output_mode(lakeshore_336, output_num, mode, input_channel) -> None: ls = lakeshore_336 mode = "off" - input_channel = "A" - powerup_enable = True - outputs = [getattr(ls, f"output_{n}") for n in range(1, 5)] - for h in outputs: # a.k.a. heaters - h.mode(mode) - h.input_channel(input_channel) - h.powerup_enable(powerup_enable) - assert h.mode() == mode - assert h.input_channel() == input_channel - assert h.powerup_enable() == powerup_enable + h = getattr(ls, f"output_{output_num}") + h.mode(mode) + h.input_channel(input_channel) + h.powerup_enable(True) + assert h.mode() == mode + assert h.input_channel() == input_channel + assert h.powerup_enable() def test_range(lakeshore_336) -> None: @@ -287,16 +100,20 @@ def test_select_range_limits(lakeshore_336) -> None: def test_set_and_wait_unit_setpoint_reached(lakeshore_336) -> None: + """Test that wait_until_set_point_reached completes in simulation mode.""" ls = lakeshore_336 ls.output_1.setpoint(4) - ls.start_heating() + # In simulation mode, wait_until_set_point_reached should return immediately + # because _is_simulated check bypasses the wait loop ls.output_1.wait_until_set_point_reached() def test_blocking_t(lakeshore_336) -> None: + """Test that blocking_t completes in simulation mode.""" ls = lakeshore_336 h = ls.output_1 ranges = [1.2, 2.4, 3.1] h.range_limits(ranges) - ls.start_heating() + # In simulation mode, blocking_t should return immediately + # because _is_simulated check bypasses the wait loop h.blocking_t(4) From 968c0f4b0365b13817d04cc8c15c958a4fff99f2 Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Fri, 31 Oct 2025 16:13:22 -0700 Subject: [PATCH 07/19] Enhance pyvisa-sim yaml for better simulation experience --- .../instrument/sims/lakeshore_model372.yaml | 994 +++++++++++++++++- 1 file changed, 992 insertions(+), 2 deletions(-) diff --git a/src/qcodes/instrument/sims/lakeshore_model372.yaml b/src/qcodes/instrument/sims/lakeshore_model372.yaml index 050c952da2e9..5968d68027ba 100644 --- a/src/qcodes/instrument/sims/lakeshore_model372.yaml +++ b/src/qcodes/instrument/sims/lakeshore_model372.yaml @@ -1,14 +1,1004 @@ spec: "1.0" + devices: device 1: eom: GPIB INSTR: q: "\r\n" r: "\r\n" - error: ERROR + error: + command error: CMD_ERROR + query error: Q_ERROR + dialogues: - q: "*IDN?" - r: "QCoDeS, m0d3l, 336, 0.0.01" + r: "QCoDeS, m0d3l, 372, 0.0.01" + + properties: + # ==================== + # Sensor Channel 1 (ch01) + # ==================== + temperature_1: + default: 4.0 + getter: + q: "KRDG? 1" + r: "{}" + setter: + q: "SIMTEMP 1,{}" + + sensor_raw_1: + default: 100.0 + getter: + q: "SRDG? 1" + r: "{}" + + sensor_status_1: + default: 0 + getter: + q: "RDGST? 1" + r: "{}" + + sensor_name_1: + default: "Channel 1" + getter: + q: "INNAME? 1" + r: "{}" + setter: + q: "INNAME 1,\"{}\"" + + sensor_tlimit_1: + default: 300.0 + getter: + q: "TLIMIT? 1" + r: "{}" + setter: + q: "TLIMIT 1,{}" + + inset_1: + default: "1,100,3,0,1" + getter: + q: "INSET? 1" + r: "{}" + setter: + q: "INSET 1,{}" + + intype_1: + default: "0,1,0,5,0,1" + getter: + q: "INTYPE? 1" + r: "{}" + setter: + q: "INTYPE 1,{}" + + # ==================== + # Sensor Channel 2 (ch02) + # ==================== + temperature_2: + default: 4.0 + getter: + q: "KRDG? 2" + r: "{}" + setter: + q: "SIMTEMP 2,{}" + + sensor_raw_2: + default: 100.0 + getter: + q: "SRDG? 2" + r: "{}" + + sensor_status_2: + default: 0 + getter: + q: "RDGST? 2" + r: "{}" + + sensor_name_2: + default: "Channel 2" + getter: + q: "INNAME? 2" + r: "{}" + setter: + q: "INNAME 2,\"{}\"" + + sensor_tlimit_2: + default: 300.0 + getter: + q: "TLIMIT? 2" + r: "{}" + setter: + q: "TLIMIT 2,{}" + + inset_2: + default: "1,100,3,0,1" + getter: + q: "INSET? 2" + r: "{}" + setter: + q: "INSET 2,{}" + + intype_2: + default: "0,1,0,5,0,1" + getter: + q: "INTYPE? 2" + r: "{}" + setter: + q: "INTYPE 2,{}" + + # ==================== + # Sensor Channel 3 (ch03) + # ==================== + temperature_3: + default: 4.0 + getter: + q: "KRDG? 3" + r: "{}" + setter: + q: "SIMTEMP 3,{}" + + sensor_raw_3: + default: 100.0 + getter: + q: "SRDG? 3" + r: "{}" + + sensor_status_3: + default: 0 + getter: + q: "RDGST? 3" + r: "{}" + + sensor_name_3: + default: "Channel 3" + getter: + q: "INNAME? 3" + r: "{}" + setter: + q: "INNAME 3,\"{}\"" + + sensor_tlimit_3: + default: 300.0 + getter: + q: "TLIMIT? 3" + r: "{}" + setter: + q: "TLIMIT 3,{}" + + inset_3: + default: "1,100,3,0,1" + getter: + q: "INSET? 3" + r: "{}" + setter: + q: "INSET 3,{}" + + intype_3: + default: "0,1,0,5,0,1" + getter: + q: "INTYPE? 3" + r: "{}" + setter: + q: "INTYPE 3,{}" + + # ==================== + # Sensor Channel 4 (ch04) + # ==================== + temperature_4: + default: 4.0 + getter: + q: "KRDG? 4" + r: "{}" + setter: + q: "SIMTEMP 4,{}" + + sensor_raw_4: + default: 100.0 + getter: + q: "SRDG? 4" + r: "{}" + + sensor_status_4: + default: 0 + getter: + q: "RDGST? 4" + r: "{}" + + sensor_name_4: + default: "Channel 4" + getter: + q: "INNAME? 4" + r: "{}" + setter: + q: "INNAME 4,\"{}\"" + + sensor_tlimit_4: + default: 300.0 + getter: + q: "TLIMIT? 4" + r: "{}" + setter: + q: "TLIMIT 4,{}" + + inset_4: + default: "1,100,3,0,1" + getter: + q: "INSET? 4" + r: "{}" + setter: + q: "INSET 4,{}" + + intype_4: + default: "0,1,0,5,0,1" + getter: + q: "INTYPE? 4" + r: "{}" + setter: + q: "INTYPE 4,{}" + + # ==================== + # Sensor Channel 5 (ch05) + # ==================== + temperature_5: + default: 4.0 + getter: + q: "KRDG? 5" + r: "{}" + setter: + q: "SIMTEMP 5,{}" + + sensor_raw_5: + default: 100.0 + getter: + q: "SRDG? 5" + r: "{}" + + sensor_status_5: + default: 0 + getter: + q: "RDGST? 5" + r: "{}" + + sensor_name_5: + default: "Channel 5" + getter: + q: "INNAME? 5" + r: "{}" + setter: + q: "INNAME 5,\"{}\"" + + sensor_tlimit_5: + default: 300.0 + getter: + q: "TLIMIT? 5" + r: "{}" + setter: + q: "TLIMIT 5,{}" + + inset_5: + default: "1,100,3,0,1" + getter: + q: "INSET? 5" + r: "{}" + setter: + q: "INSET 5,{}" + + intype_5: + default: "0,1,0,5,0,1" + getter: + q: "INTYPE? 5" + r: "{}" + setter: + q: "INTYPE 5,{}" + + # ==================== + # Sensor Channel 6 (ch06) + # ==================== + temperature_6: + default: 4.0 + getter: + q: "KRDG? 6" + r: "{}" + setter: + q: "SIMTEMP 6,{}" + + sensor_raw_6: + default: 100.0 + getter: + q: "SRDG? 6" + r: "{}" + + sensor_status_6: + default: 0 + getter: + q: "RDGST? 6" + r: "{}" + + sensor_name_6: + default: "Channel 6" + getter: + q: "INNAME? 6" + r: "{}" + setter: + q: "INNAME 6,\"{}\"" + + sensor_tlimit_6: + default: 300.0 + getter: + q: "TLIMIT? 6" + r: "{}" + setter: + q: "TLIMIT 6,{}" + + inset_6: + default: "1,100,3,0,1" + getter: + q: "INSET? 6" + r: "{}" + setter: + q: "INSET 6,{}" + + intype_6: + default: "0,1,0,5,0,1" + getter: + q: "INTYPE? 6" + r: "{}" + setter: + q: "INTYPE 6,{}" + + # ==================== + # Sensor Channel 7 (ch07) + # ==================== + temperature_7: + default: 4.0 + getter: + q: "KRDG? 7" + r: "{}" + setter: + q: "SIMTEMP 7,{}" + + sensor_raw_7: + default: 100.0 + getter: + q: "SRDG? 7" + r: "{}" + + sensor_status_7: + default: 0 + getter: + q: "RDGST? 7" + r: "{}" + + sensor_name_7: + default: "Channel 7" + getter: + q: "INNAME? 7" + r: "{}" + setter: + q: "INNAME 7,\"{}\"" + + sensor_tlimit_7: + default: 300.0 + getter: + q: "TLIMIT? 7" + r: "{}" + setter: + q: "TLIMIT 7,{}" + + inset_7: + default: "1,100,3,0,1" + getter: + q: "INSET? 7" + r: "{}" + setter: + q: "INSET 7,{}" + + intype_7: + default: "0,1,0,5,0,1" + getter: + q: "INTYPE? 7" + r: "{}" + setter: + q: "INTYPE 7,{}" + + # ==================== + # Sensor Channel 8 (ch08) + # ==================== + temperature_8: + default: 4.0 + getter: + q: "KRDG? 8" + r: "{}" + setter: + q: "SIMTEMP 8,{}" + + sensor_raw_8: + default: 100.0 + getter: + q: "SRDG? 8" + r: "{}" + + sensor_status_8: + default: 0 + getter: + q: "RDGST? 8" + r: "{}" + + sensor_name_8: + default: "Channel 8" + getter: + q: "INNAME? 8" + r: "{}" + setter: + q: "INNAME 8,\"{}\"" + + sensor_tlimit_8: + default: 300.0 + getter: + q: "TLIMIT? 8" + r: "{}" + setter: + q: "TLIMIT 8,{}" + + inset_8: + default: "1,100,3,0,1" + getter: + q: "INSET? 8" + r: "{}" + setter: + q: "INSET 8,{}" + + intype_8: + default: "0,1,0,5,0,1" + getter: + q: "INTYPE? 8" + r: "{}" + setter: + q: "INTYPE 8,{}" + + # ==================== + # Sensor Channel 9 (ch09) + # ==================== + temperature_9: + default: 4.0 + getter: + q: "KRDG? 9" + r: "{}" + setter: + q: "SIMTEMP 9,{}" + + sensor_raw_9: + default: 100.0 + getter: + q: "SRDG? 9" + r: "{}" + + sensor_status_9: + default: 0 + getter: + q: "RDGST? 9" + r: "{}" + + sensor_name_9: + default: "Channel 9" + getter: + q: "INNAME? 9" + r: "{}" + setter: + q: "INNAME 9,\"{}\"" + + sensor_tlimit_9: + default: 300.0 + getter: + q: "TLIMIT? 9" + r: "{}" + setter: + q: "TLIMIT 9,{}" + + inset_9: + default: "1,100,3,0,1" + getter: + q: "INSET? 9" + r: "{}" + setter: + q: "INSET 9,{}" + + intype_9: + default: "0,1,0,5,0,1" + getter: + q: "INTYPE? 9" + r: "{}" + setter: + q: "INTYPE 9,{}" + + # ==================== + # Sensor Channel 10 (ch10) + # ==================== + temperature_10: + default: 4.0 + getter: + q: "KRDG? 10" + r: "{}" + setter: + q: "SIMTEMP 10,{}" + + sensor_raw_10: + default: 100.0 + getter: + q: "SRDG? 10" + r: "{}" + + sensor_status_10: + default: 0 + getter: + q: "RDGST? 10" + r: "{}" + + sensor_name_10: + default: "Channel 10" + getter: + q: "INNAME? 10" + r: "{}" + setter: + q: "INNAME 10,\"{}\"" + + sensor_tlimit_10: + default: 300.0 + getter: + q: "TLIMIT? 10" + r: "{}" + setter: + q: "TLIMIT 10,{}" + + inset_10: + default: "1,100,3,0,1" + getter: + q: "INSET? 10" + r: "{}" + setter: + q: "INSET 10,{}" + + intype_10: + default: "0,1,0,5,0,1" + getter: + q: "INTYPE? 10" + r: "{}" + setter: + q: "INTYPE 10,{}" + + # ==================== + # Sensor Channel 11 (ch11) + # ==================== + temperature_11: + default: 4.0 + getter: + q: "KRDG? 11" + r: "{}" + setter: + q: "SIMTEMP 11,{}" + + sensor_raw_11: + default: 100.0 + getter: + q: "SRDG? 11" + r: "{}" + + sensor_status_11: + default: 0 + getter: + q: "RDGST? 11" + r: "{}" + + sensor_name_11: + default: "Channel 11" + getter: + q: "INNAME? 11" + r: "{}" + setter: + q: "INNAME 11,\"{}\"" + + sensor_tlimit_11: + default: 300.0 + getter: + q: "TLIMIT? 11" + r: "{}" + setter: + q: "TLIMIT 11,{}" + + inset_11: + default: "1,100,3,0,1" + getter: + q: "INSET? 11" + r: "{}" + setter: + q: "INSET 11,{}" + + intype_11: + default: "0,1,0,5,0,1" + getter: + q: "INTYPE? 11" + r: "{}" + setter: + q: "INTYPE 11,{}" + + # ==================== + # Sensor Channel 12 (ch12) + # ==================== + temperature_12: + default: 4.0 + getter: + q: "KRDG? 12" + r: "{}" + setter: + q: "SIMTEMP 12,{}" + + sensor_raw_12: + default: 100.0 + getter: + q: "SRDG? 12" + r: "{}" + + sensor_status_12: + default: 0 + getter: + q: "RDGST? 12" + r: "{}" + + sensor_name_12: + default: "Channel 12" + getter: + q: "INNAME? 12" + r: "{}" + setter: + q: "INNAME 12,\"{}\"" + + sensor_tlimit_12: + default: 300.0 + getter: + q: "TLIMIT? 12" + r: "{}" + setter: + q: "TLIMIT 12,{}" + + inset_12: + default: "1,100,3,0,1" + getter: + q: "INSET? 12" + r: "{}" + setter: + q: "INSET 12,{}" + + intype_12: + default: "0,1,0,5,0,1" + getter: + q: "INTYPE? 12" + r: "{}" + setter: + q: "INTYPE 12,{}" + + # ==================== + # Sensor Channel 13 (ch13) + # ==================== + temperature_13: + default: 4.0 + getter: + q: "KRDG? 13" + r: "{}" + setter: + q: "SIMTEMP 13,{}" + + sensor_raw_13: + default: 100.0 + getter: + q: "SRDG? 13" + r: "{}" + + sensor_status_13: + default: 0 + getter: + q: "RDGST? 13" + r: "{}" + + sensor_name_13: + default: "Channel 13" + getter: + q: "INNAME? 13" + r: "{}" + setter: + q: "INNAME 13,\"{}\"" + + sensor_tlimit_13: + default: 300.0 + getter: + q: "TLIMIT? 13" + r: "{}" + setter: + q: "TLIMIT 13,{}" + + inset_13: + default: "1,100,3,0,1" + getter: + q: "INSET? 13" + r: "{}" + setter: + q: "INSET 13,{}" + + intype_13: + default: "0,1,0,5,0,1" + getter: + q: "INTYPE? 13" + r: "{}" + setter: + q: "INTYPE 13,{}" + + # ==================== + # Sensor Channel 14 (ch14) + # ==================== + temperature_14: + default: 4.0 + getter: + q: "KRDG? 14" + r: "{}" + setter: + q: "SIMTEMP 14,{}" + + sensor_raw_14: + default: 100.0 + getter: + q: "SRDG? 14" + r: "{}" + + sensor_status_14: + default: 0 + getter: + q: "RDGST? 14" + r: "{}" + + sensor_name_14: + default: "Channel 14" + getter: + q: "INNAME? 14" + r: "{}" + setter: + q: "INNAME 14,\"{}\"" + + sensor_tlimit_14: + default: 300.0 + getter: + q: "TLIMIT? 14" + r: "{}" + setter: + q: "TLIMIT 14,{}" + + inset_14: + default: "1,100,3,0,1" + getter: + q: "INSET? 14" + r: "{}" + setter: + q: "INSET 14,{}" + + intype_14: + default: "0,1,0,5,0,1" + getter: + q: "INTYPE? 14" + r: "{}" + setter: + q: "INTYPE 14,{}" + + # ==================== + # Sensor Channel 15 (ch15) + # ==================== + temperature_15: + default: 4.0 + getter: + q: "KRDG? 15" + r: "{}" + setter: + q: "SIMTEMP 15,{}" + + sensor_raw_15: + default: 100.0 + getter: + q: "SRDG? 15" + r: "{}" + + sensor_status_15: + default: 0 + getter: + q: "RDGST? 15" + r: "{}" + + sensor_name_15: + default: "Channel 15" + getter: + q: "INNAME? 15" + r: "{}" + setter: + q: "INNAME 15,\"{}\"" + + sensor_tlimit_15: + default: 300.0 + getter: + q: "TLIMIT? 15" + r: "{}" + setter: + q: "TLIMIT 15,{}" + + inset_15: + default: "1,100,3,0,1" + getter: + q: "INSET? 15" + r: "{}" + setter: + q: "INSET 15,{}" + + intype_15: + default: "0,1,0,5,0,1" + getter: + q: "INTYPE? 15" + r: "{}" + setter: + q: "INTYPE 15,{}" + + # ==================== + # Sensor Channel 16 (ch16) + # ==================== + temperature_16: + default: 4.0 + getter: + q: "KRDG? 16" + r: "{}" + setter: + q: "SIMTEMP 16,{}" + + sensor_raw_16: + default: 100.0 + getter: + q: "SRDG? 16" + r: "{}" + + sensor_status_16: + default: 0 + getter: + q: "RDGST? 16" + r: "{}" + + sensor_name_16: + default: "Channel 16" + getter: + q: "INNAME? 16" + r: "{}" + setter: + q: "INNAME 16,\"{}\"" + + sensor_tlimit_16: + default: 300.0 + getter: + q: "TLIMIT? 16" + r: "{}" + setter: + q: "TLIMIT 16,{}" + + inset_16: + default: "1,100,3,0,1" + getter: + q: "INSET? 16" + r: "{}" + setter: + q: "INSET 16,{}" + + intype_16: + default: "0,1,0,5,0,1" + getter: + q: "INTYPE? 16" + r: "{}" + setter: + q: "INTYPE 16,{}" + + # ==================== + # Heater Output 0 (sample_heater) + # ==================== + outmode_output_0: + default: "5,2,0,0,0,1" + getter: + q: "OUTMODE? 0" + r: "{}" + setter: + q: "OUTMODE 0,{}" + + pid_output_0: + default: "10,20,30" + getter: + q: "PID? 0" + r: "{}" + setter: + q: "PID 0,{}" + + range_output_0: + default: 0 + getter: + q: "RANGE? 0" + r: "{}" + setter: + q: "RANGE 0,{}" + + setpoint_output_0: + default: 4.0 + getter: + q: "SETP? 0" + r: "{}" + setter: + q: "SETP 0,{}" + + # ==================== + # Heater Output 1 (warmup_heater) + # ==================== + outmode_output_1: + default: "5,2,0,0,0,1" + getter: + q: "OUTMODE? 1" + r: "{}" + setter: + q: "OUTMODE 1,{}" + + pid_output_1: + default: "1,2,3" + getter: + q: "PID? 1" + r: "{}" + setter: + q: "PID 1,{}" + + range_output_1: + default: 0 + getter: + q: "RANGE? 1" + r: "{}" + setter: + q: "RANGE 1,{}" + + setpoint_output_1: + default: 4.0 + getter: + q: "SETP? 1" + r: "{}" + setter: + q: "SETP 1,{}" + + # ==================== + # Heater Output 2 (analog_heater) + # ==================== + outmode_output_2: + default: "5,2,0,0,0,1" + getter: + q: "OUTMODE? 2" + r: "{}" + setter: + q: "OUTMODE 2,{}" + + pid_output_2: + default: "10,20,30" + getter: + q: "PID? 2" + r: "{}" + setter: + q: "PID 2,{}" + + range_output_2: + default: 0 + getter: + q: "RANGE? 2" + r: "{}" + setter: + q: "RANGE 2,{}" + + setpoint_output_2: + default: 4.0 + getter: + q: "SETP? 2" + r: "{}" + setter: + q: "SETP 2,{}" resources: GPIB::3::INSTR: From 79ab49d2b94bc8d635cd868f363846a29d8b511e Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Fri, 31 Oct 2025 16:20:03 -0700 Subject: [PATCH 08/19] Use pyvisa-sim instead of special mock class. --- tests/drivers/test_lakeshore_372.py | 338 +--------------------------- 1 file changed, 2 insertions(+), 336 deletions(-) diff --git a/tests/drivers/test_lakeshore_372.py b/tests/drivers/test_lakeshore_372.py index 4f59e3f3d1ae..03a75008c775 100644 --- a/tests/drivers/test_lakeshore_372.py +++ b/tests/drivers/test_lakeshore_372.py @@ -1,350 +1,19 @@ from __future__ import annotations -import logging -import time -import warnings -from contextlib import suppress -from functools import wraps -from typing import TYPE_CHECKING, Any, Literal, TypeVar +from typing import Literal, TypeVar import pytest from typing_extensions import ParamSpec -from qcodes.instrument import InstrumentBase from qcodes.instrument_drivers.Lakeshore import LakeshoreModel372 from qcodes.instrument_drivers.Lakeshore.lakeshore_base import ( LakeshoreBaseSensorChannel, ) -from qcodes.logger import get_instrument_logger -from qcodes.utils import QCoDeSDeprecationWarning - -if TYPE_CHECKING: - from collections.abc import Callable - -log = logging.getLogger(__name__) - -VISA_LOGGER = ".".join((InstrumentBase.__module__, "com", "visa")) P = ParamSpec("P") T = TypeVar("T") -class MockVisaInstrument: - """ - Mixin class that overrides write_raw and ask_raw to simulate an - instrument. - """ - - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - self.visa_log = get_instrument_logger(self, VISA_LOGGER) # type: ignore[arg-type] - - # This base class mixin holds two dictionaries associated with the - # pyvisa_instrument.write() - self.cmds: dict[str, Callable[..., Any]] = {} - # and pyvisa_instrument.query() functions - self.queries: dict[str, Callable[..., Any]] = {} - # the keys are the issued VISA commands like '*IDN?' or '*OPC' - # the values are the corresponding methods to be called on the mock - # instrument. - - # To facilitate the definition there are the decorators `@query' and - # `@command`. These attach an attribute to the method, so that the - # dictionaries can be filled here in the constructor. (This is - # borderline abusive, but makes a it easy to define mocks) - func_names = dir(self) - # cycle through all methods - for func_name in func_names: - with warnings.catch_warnings(): - if func_name == "_name": - # silence warning when getting deprecated attribute - warnings.simplefilter("ignore", category=QCoDeSDeprecationWarning) - - f = getattr(self, func_name) - # only add for methods that have such an attribute - with suppress(AttributeError): - self.queries[getattr(f, "query_name")] = f - with suppress(AttributeError): - self.cmds[getattr(f, "command_name")] = f - - def write_raw(self, cmd) -> None: - cmd_parts = cmd.split(" ") - cmd_str = cmd_parts[0].upper() - if cmd_str in self.cmds: - args = "".join(cmd_parts[1:]) - self.visa_log.debug(f"Query: {cmd} for command {cmd_str} with args {args}") - self.cmds[cmd_str](args) - else: - super().write_raw(cmd) # type: ignore[misc] - - def ask_raw(self, cmd) -> Any: - query_parts = cmd.split(" ") - query_str = query_parts[0].upper() - if query_str in self.queries: - args = "".join(query_parts[1:]) - self.visa_log.debug( - f"Query: {cmd} for command {query_str} with args {args}" - ) - response = self.queries[query_str](args) - self.visa_log.debug(f"Response: {response}") - return response - else: - return super().ask_raw(cmd) # type: ignore[misc] - - -def query(name: str) -> Callable[[Callable[P, T]], Callable[P, T]]: - def wrapper(func: Callable[P, T]) -> Callable[P, T]: - func.query_name = name.upper() # type: ignore[attr-defined] - return func - - return wrapper - - -def command(name: str) -> Callable[[Callable[P, T]], Callable[P, T]]: - def wrapper(func: Callable[P, T]) -> Callable[P, T]: - func.command_name = name.upper() # type: ignore[attr-defined] - return func - - return wrapper - - -def split_args(split_char: str = ","): - def wrapper(func): - @wraps(func) - def decorated_func(self, string_arg): - args = string_arg.split(split_char) - return func(self, *args) - - return decorated_func - - return wrapper - - -class DictClass: - def __init__(self, **kwargs): - # https://stackoverflow.com/questions/16237659/python-how-to-implement-getattr - super().__setattr__("_attrs", kwargs) - - for kwarg, value in kwargs.items(): - self._attrs[kwarg] = value - - def __getattr__(self, attr): - try: - return self._attrs[attr] - except KeyError as e: - raise AttributeError from e - - def __setattr__(self, name: str, value: Any) -> None: - self._attrs[name] = value - - -class LakeshoreModel372Mock(MockVisaInstrument, LakeshoreModel372): - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - - # initial values - self.heaters: dict[str, DictClass] = {} - self.heaters["0"] = DictClass( - P=1, - I=2, - D=3, - mode=5, - input_channel=2, - powerup_enable=0, - polarity=0, - use_filter=0, - delay=1, - output_range=0, - setpoint=4, - ) - self.heaters["1"] = DictClass( - P=1, - I=2, - D=3, - mode=5, - input_channel=2, - powerup_enable=0, - polarity=0, - use_filter=0, - delay=1, - output_range=0, - setpoint=4, - ) - self.heaters["2"] = DictClass( - P=1, - I=2, - D=3, - mode=5, - input_channel=2, - powerup_enable=0, - polarity=0, - use_filter=0, - delay=1, - output_range=0, - setpoint=4, - ) - - self.channel_mock = { - str(i): DictClass( - tlimit=i, - T=4, - enabled=1, # True - dwell=100, - pause=3, - curve_number=0, - temperature_coefficient=1, # 'negative', - excitation_mode=0, #'voltage', - excitation_range_number=1, - auto_range=0, #'off', - range=5, #'200 mOhm', - current_source_shunted=0, # False, - units=1, - ) #'kelvin') - for i in range(1, 17) - } - - # simulate delayed heating - self.simulate_heating = False - self.start_heating_time = time.perf_counter() - - def start_heating(self): - self.start_heating_time = time.perf_counter() - self.simulate_heating = True - - def get_t_when_heating(self): - """ - Simply define a fixed setpoint of 4 k for now - """ - delta = abs(time.perf_counter() - self.start_heating_time) - # make it simple to start with: linear ramp 1K per second - # start at 7K. - return max(4, 7 - delta) - - @query("PID?") - def pidq(self, arg): - heater = self.heaters[arg] - return f"{heater.P},{heater.I},{heater.D}" - - @command("PID") - @split_args() - def pid(self, output, P, I, D): # noqa E741 - for a, v in zip(["P", "I", "D"], [P, I, D]): - setattr(self.heaters[output], a, v) - - @query("OUTMODE?") - def outmodeq(self, arg): - heater = self.heaters[arg] - return ( - f"{heater.mode},{heater.input_channel}," - f"{heater.powerup_enable},{heater.polarity}," - f"{heater.use_filter},{heater.delay}" - ) - - @command("OUTMODE") - @split_args() - def outputmode( - self, output, mode, input_channel, powerup_enable, polarity, use_filter, delay - ): - h = self.heaters[output] - h.output = output - h.mode = mode - h.input_channel = input_channel - h.powerup_enable = powerup_enable - h.polarity = polarity - h.use_filter = use_filter - h.delay = delay - - @query("INSET?") - def insetq(self, channel): - ch = self.channel_mock[channel] - return ( - f"{ch.enabled},{ch.dwell}," - f"{ch.pause},{ch.curve_number}," - f"{ch.temperature_coefficient}" - ) - - @command("INSET") - @split_args() - def inset( - self, channel, enabled, dwell, pause, curve_number, temperature_coefficient - ): - ch = self.channel_mock[channel] - ch.enabled = enabled - ch.dwell = dwell - ch.pause = pause - ch.curve_number = curve_number - ch.temperature_coefficient = temperature_coefficient - - @query("INTYPE?") - def intypeq(self, channel): - ch = self.channel_mock[channel] - return ( - f"{ch.excitation_mode},{ch.excitation_range_number}," - f"{ch.auto_range},{ch.range}," - f"{ch.current_source_shunted},{ch.units}" - ) - - @command("INTYPE") - @split_args() - def intype( - self, - channel, - excitation_mode, - excitation_range_number, - auto_range, - range, - current_source_shunted, - units, - ): - ch = self.channel_mock[channel] - ch.excitation_mode = excitation_mode - ch.excitation_range_number = excitation_range_number - ch.auto_range = auto_range - ch.range = range - ch.current_source_shunted = current_source_shunted - ch.units = units - - @query("RANGE?") - def rangeq(self, heater): - h = self.heaters[heater] - return f"{h.output_range}" - - @command("RANGE") - @split_args() - def range_cmd(self, heater, output_range): - h = self.heaters[heater] - h.output_range = output_range - - @query("SETP?") - def setpointq(self, heater): - h = self.heaters[heater] - return f"{h.setpoint}" - - @command("SETP") - @split_args() - def setpoint(self, heater, setpoint): - h = self.heaters[heater] - h.setpoint = setpoint - - @query("TLIMIT?") - def tlimitq(self, channel): - chan = self.channel_mock[channel] - return f"{chan.tlimit}" - - @command("TLIMIT") - @split_args() - def tlimitcmd(self, channel, tlimit): - chan = self.channel_mock[channel] - chan.tlimit = tlimit - - @query("KRDG?") - def temperature(self, output): - chan = self.channel_mock[output] - if self.simulate_heating: - return self.get_t_when_heating() - return f"{chan.T}" - - def instrument_fixture( scope: Literal["session", "package", "module", "class", "function"] = "function", name=None, @@ -365,7 +34,7 @@ def wrapped_fixture(): @instrument_fixture(scope="function") def lakeshore_372(): - return LakeshoreModel372Mock( + return LakeshoreModel372( "lakeshore_372_fixture", "GPIB::3::INSTR", pyvisa_sim_file="lakeshore_model372.yaml", @@ -446,16 +115,13 @@ def test_select_range_limits(lakeshore_372) -> None: def test_set_and_wait_unit_setpoint_reached(lakeshore_372) -> None: ls = lakeshore_372 ls.sample_heater.setpoint(4) - ls.start_heating() ls.sample_heater.wait_until_set_point_reached() def test_blocking_t(lakeshore_372) -> None: - ls = lakeshore_372 h = lakeshore_372.sample_heater ranges = list(range(1, 9)) h.range_limits(ranges) - ls.start_heating() h.blocking_t(4) From 590c31f2c0121fad2292bca2d6d648ade7687e0b Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Mon, 3 Nov 2025 12:00:36 -0800 Subject: [PATCH 09/19] Improve pyvisa-sim yaml for lakeshore model 335 --- .../instrument/sims/lakeshore_model335.yaml | 172 +++++++++++++++++- 1 file changed, 164 insertions(+), 8 deletions(-) diff --git a/src/qcodes/instrument/sims/lakeshore_model335.yaml b/src/qcodes/instrument/sims/lakeshore_model335.yaml index 1166b1560fce..9c7755fc8423 100644 --- a/src/qcodes/instrument/sims/lakeshore_model335.yaml +++ b/src/qcodes/instrument/sims/lakeshore_model335.yaml @@ -16,6 +16,8 @@ devices: getter: q: "KRDG? A" r: "{}" + setter: + q: "SIMTEMP A,{}" # Custom simulation-only command sensor_raw_A: default: 101.0 @@ -37,21 +39,37 @@ devices: setter: q: "INNAME A,\"{}\"" + sensor_tlimit_A: + default: "300.0" + getter: + q: "TLIMIT? A" + r: "{}" + setter: + q: "TLIMIT A,{}" + + sensor_type_A: + default: "1,0,1,0,1" + getter: + q: "INTYPE? A" + r: "{}" + setter: + q: "INTYPE A,{}" + sensor_setpoint_A: default: "100" getter: - q: "setp? A" + q: "SETP? A" r: "{}" setter: - q: "setp A,\"{}\"" + q: "SETP A,\"{}\"" sensor_range_A: default: "1" getter: - q: "range? A" + q: "RANGE? A" r: "{}" setter: - q: "range A,\"{}\"" + q: "RANGE A,\"{}\"" temperature_B: @@ -59,6 +77,8 @@ devices: getter: q: "KRDG? B" r: "{}" + setter: + q: "SIMTEMP B,{}" # Custom simulation-only command sensor_raw_B: default: 101.0 @@ -80,21 +100,157 @@ devices: setter: q: "INNAME B,\"{}\"" + sensor_tlimit_B: + default: "300.0" + getter: + q: "TLIMIT? B" + r: "{}" + setter: + q: "TLIMIT B,{}" + + sensor_type_B: + default: "1,0,1,0,1" + getter: + q: "INTYPE? B" + r: "{}" + setter: + q: "INTYPE B,{}" + sensor_setpoint_B: default: "100" getter: - q: "setp? A" + q: "SETP? B" r: "{}" setter: - q: "setp A,\"{}\"" + q: "SETP B,\"{}\"" sensor_range_B: default: "1" getter: - q: "range? A" + q: "RANGE? B" + r: "{}" + setter: + q: "RANGE B,\"{}\"" + + output_mode_1: + default: "1,1,0" + getter: + q: "OUTMODE? 1" + r: "{}" + setter: + q: "OUTMODE 1,{}" + + output_mode_2: + default: "1,2,0" + getter: + q: "OUTMODE? 2" + r: "{}" + setter: + q: "OUTMODE 2,{}" + + pid_output_1: + default: "10,20,30" + getter: + q: "PID? 1" + r: "{}" + setter: + q: "PID 1,{}" + + pid_output_2: + default: "10,20,30" + getter: + q: "PID? 2" + r: "{}" + setter: + q: "PID 2,{}" + + output_range_1: + default: "1" + getter: + q: "RANGE? 1" + r: "{}" + setter: + q: "RANGE 1,{}" + + output_range_2: + default: "1" + getter: + q: "RANGE? 2" + r: "{}" + setter: + q: "RANGE 2,{}" + + heater_output_1: + default: "0.0" + getter: + q: "HTR? 1" + r: "{}" + + heater_output_2: + default: "0.0" + getter: + q: "HTR? 2" + r: "{}" + + output_setpoint_1: + default: "100.0" + getter: + q: "SETP? 1" + r: "{}" + setter: + q: "SETP 1,{}" + + output_setpoint_2: + default: "100.0" + getter: + q: "SETP? 2" + r: "{}" + setter: + q: "SETP 2,{}" + + heater_setup_1: + default: "0,1,0,0.0,1" + getter: + q: "HTRSET? 1" + r: "{}" + setter: + q: "HTRSET 1,{}" + + heater_setup_2: + default: "0,1,0,0.0,1" + getter: + q: "HTRSET? 2" + r: "{}" + setter: + q: "HTRSET 2,{}" + + setpoint_ramp_1: + default: "0,0.0" + getter: + q: "RAMP? 1" r: "{}" setter: - q: "range A,\"{}\"" + q: "RAMP 1,{}" + + setpoint_ramp_2: + default: "0,0.0" + getter: + q: "RAMP? 2" + r: "{}" + setter: + q: "RAMP 2,{}" + + setpoint_ramp_status_1: + default: "0" + getter: + q: "RAMPST? 1" + r: "{}" + + setpoint_ramp_status_2: + default: "0" + getter: + q: "RAMPST? 2" + r: "{}" resources: From decb90cbdda4c9f0fa7d3c6c9b02d110d38db3e8 Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Mon, 3 Nov 2025 12:04:55 -0800 Subject: [PATCH 10/19] Update Lakeshore335 tests to use pyvisa-sim backend instead of mocked class. --- tests/drivers/test_lakeshore_335.py | 180 +--------------------------- 1 file changed, 3 insertions(+), 177 deletions(-) diff --git a/tests/drivers/test_lakeshore_335.py b/tests/drivers/test_lakeshore_335.py index 0120707befa9..f51f3a987ccd 100644 --- a/tests/drivers/test_lakeshore_335.py +++ b/tests/drivers/test_lakeshore_335.py @@ -1,183 +1,11 @@ -import logging -import time +import pytest -from qcodes.instrument import InstrumentBase from qcodes.instrument_drivers.Lakeshore import LakeshoreModel335 -from .test_lakeshore_372 import ( - DictClass, - MockVisaInstrument, - command, - instrument_fixture, - query, - split_args, -) -log = logging.getLogger(__name__) - -VISA_LOGGER = ".".join((InstrumentBase.__module__, "com", "visa")) - - -class LakeshoreModel335Mock(MockVisaInstrument, LakeshoreModel335): - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - - # initial values - self.heaters: dict[str, DictClass] = {} - self.heaters["1"] = DictClass( - P=1, - I=2, - D=3, - mode=1, # 'off' - input_channel=1, # 'A' - powerup_enable=0, - polarity=0, - use_filter=0, - delay=1, - output_range=0, - setpoint=4, - ) - self.heaters["2"] = DictClass( - P=1, - I=2, - D=3, - mode=2, # 'closed_loop' - input_channel=2, # 'B' - powerup_enable=0, - polarity=0, - use_filter=0, - delay=1, - output_range=0, - setpoint=4, - ) - - self.channel_mock = { - str(i): DictClass( - t_limit=i, - T=4, - sensor_name=f"sensor_{i}", - sensor_type=1, # 'diode', - auto_range_enabled=0, # 'off', - range=0, - compensation_enabled=0, # False, - units=1, - ) # 'kelvin') - for i in self.channel_name_command.keys() - } - - # simulate delayed heating - self.simulate_heating = False - self.start_heating_time = time.perf_counter() - - def start_heating(self): - self.start_heating_time = time.perf_counter() - self.simulate_heating = True - - def get_t_when_heating(self): - """ - Simply define a fixed setpoint of 4 k for now - """ - delta = abs(time.perf_counter() - self.start_heating_time) - # make it simple to start with: linear ramp 1K per second - # start at 7K. - return max(4, 7 - delta) - - @query("PID?") - def pidq(self, arg): - heater = self.heaters[arg] - return f"{heater.P},{heater.I},{heater.D}" - - @command("PID") - @split_args() - def pid(self, output, P, I, D): # noqa E741 - for a, v in zip(["P", "I", "D"], [P, I, D]): - setattr(self.heaters[output], a, v) - - @query("OUTMODE?") - def outmodeq(self, arg): - heater = self.heaters[arg] - return f"{heater.mode},{heater.input_channel},{heater.powerup_enable}" - - @command("OUTMODE") - @split_args() - def outputmode(self, output, mode, input_channel, powerup_enable): - h = self.heaters[output] - h.output = output - h.mode = mode - h.input_channel = input_channel - h.powerup_enable = powerup_enable - - @query("INTYPE?") - def intypeq(self, channel): - ch = self.channel_mock[channel] - return ( - f"{ch.sensor_type}," - f"{ch.auto_range_enabled},{ch.range}," - f"{ch.compensation_enabled},{ch.units}" - ) - - @command("INTYPE") - @split_args() - def intype( - self, - channel, - sensor_type, - auto_range_enabled, - range_, - compensation_enabled, - units, - ): - ch = self.channel_mock[channel] - ch.sensor_type = sensor_type - ch.auto_range_enabled = auto_range_enabled - ch.range = range_ - ch.compensation_enabled = compensation_enabled - ch.units = units - - @query("RANGE?") - def rangeq(self, heater): - h = self.heaters[heater] - return f"{h.output_range}" - - @command("RANGE") - @split_args() - def range_cmd(self, heater, output_range): - h = self.heaters[heater] - h.output_range = output_range - - @query("SETP?") - def setpointq(self, heater): - h = self.heaters[heater] - return f"{h.setpoint}" - - @command("SETP") - @split_args() - def setpoint(self, heater, setpoint): - h = self.heaters[heater] - h.setpoint = setpoint - - @query("TLIMIT?") - def tlimitq(self, channel): - chan = self.channel_mock[channel] - return f"{chan.tlimit}" - - @command("TLIMIT") - @split_args() - def tlimitcmd(self, channel, tlimit): - chan = self.channel_mock[channel] - chan.tlimit = tlimit - - @query("KRDG?") - def temperature(self, output): - chan = self.channel_mock[output] - if self.simulate_heating: - return self.get_t_when_heating() - return f"{chan.T}" - - -@instrument_fixture(scope="function", name="lakeshore_335") +@pytest.fixture(scope="function", name="lakeshore_335") def _make_lakeshore_335(): - return LakeshoreModel335Mock( + return LakeshoreModel335( "lakeshore_335_fixture", "GPIB::2::INSTR", pyvisa_sim_file="lakeshore_model335.yaml", @@ -256,7 +84,6 @@ def test_select_range_limits(lakeshore_335) -> None: def test_set_and_wait_unit_setpoint_reached(lakeshore_335) -> None: ls = lakeshore_335 ls.output_1.setpoint(4) - ls.start_heating() ls.output_1.wait_until_set_point_reached() @@ -265,5 +92,4 @@ def test_blocking_t(lakeshore_335) -> None: h = ls.output_1 ranges = [1.2, 2.4, 3.1] h.range_limits(ranges) - ls.start_heating() h.blocking_t(4) From 277a45776d093136c17a611b139e09cce2b759e0 Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Mon, 3 Nov 2025 12:52:28 -0800 Subject: [PATCH 11/19] Update lakeshore 336 legacy tests to use new pyvisa-sim file. --- tests/drivers/test_lakeshore_336_legacy.py | 200 +-------------------- 1 file changed, 3 insertions(+), 197 deletions(-) diff --git a/tests/drivers/test_lakeshore_336_legacy.py b/tests/drivers/test_lakeshore_336_legacy.py index 3501ce2b3f5b..337f63062c03 100644 --- a/tests/drivers/test_lakeshore_336_legacy.py +++ b/tests/drivers/test_lakeshore_336_legacy.py @@ -1,205 +1,13 @@ -import logging -import time +import pytest -from qcodes.instrument import InstrumentBase from qcodes.instrument_drivers.Lakeshore.Model_336 import ( Model_336, # pyright: ignore[reportDeprecated] ) -from .test_lakeshore_372 import ( - DictClass, - MockVisaInstrument, - command, - instrument_fixture, - query, - split_args, -) - -log = logging.getLogger(__name__) - -VISA_LOGGER = ".".join((InstrumentBase.__module__, "com", "visa")) - - -class Model_336_Mock(MockVisaInstrument, Model_336): # pyright: ignore[reportDeprecated] - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - - # initial values - self.heaters: dict[str, DictClass] = {} - self.heaters["1"] = DictClass( - P=1, - I=2, - D=3, - mode=1, # 'off' - input_channel=1, # 'A' - powerup_enable=0, - polarity=0, - use_filter=0, - delay=1, - output_range=0, - setpoint=4, - ) - self.heaters["2"] = DictClass( - P=1, - I=2, - D=3, - mode=2, # 'closed_loop' - input_channel=2, # 'B' - powerup_enable=0, - polarity=0, - use_filter=0, - delay=1, - output_range=0, - setpoint=4, - ) - self.heaters["3"] = DictClass( - mode=4, # 'monitor_out' - input_channel=2, # 'B' - powerup_enable=0, - polarity=0, - use_filter=0, - delay=1, - output_range=0, - setpoint=4, - ) - self.heaters["4"] = DictClass( - mode=5, # 'warm_up' - input_channel=1, # 'A' - powerup_enable=0, - polarity=0, - use_filter=0, - delay=1, - output_range=0, - setpoint=4, - ) - - self.channel_mock = { - str(i): DictClass( - t_limit=i, - T=4, - sensor_name=f"sensor_{i}", - sensor_type=1, # 'diode', - auto_range_enabled=0, # 'off', - range=0, - compensation_enabled=0, # False, - units=1, - ) # 'kelvin') - for i in self.channel_name_command.keys() - } - - # simulate delayed heating - self.simulate_heating = False - self.start_heating_time = time.perf_counter() - - def start_heating(self): - self.start_heating_time = time.perf_counter() - self.simulate_heating = True - - def get_t_when_heating(self): - """ - Simply define a fixed setpoint of 4 k for now - """ - delta = abs(time.perf_counter() - self.start_heating_time) - # make it simple to start with: linear ramp 1K per second - # start at 7K. - return max(4, 7 - delta) - - @query("PID?") - def pidq(self, arg): - heater = self.heaters[arg] - return f"{heater.P},{heater.I},{heater.D}" - - @command("PID") - @split_args() - def pid(self, output, P, I, D): # noqa E741 - for a, v in zip(["P", "I", "D"], [P, I, D]): - setattr(self.heaters[output], a, v) - - @query("OUTMODE?") - def outmodeq(self, arg): - heater = self.heaters[arg] - return f"{heater.mode},{heater.input_channel},{heater.powerup_enable}" - - @command("OUTMODE") - @split_args() - def outputmode(self, output, mode, input_channel, powerup_enable): - h = self.heaters[output] - h.output = output - h.mode = mode - h.input_channel = input_channel - h.powerup_enable = powerup_enable - - @query("INTYPE?") - def intypeq(self, channel): - ch = self.channel_mock[channel] - return ( - f"{ch.sensor_type}," - f"{ch.auto_range_enabled},{ch.range}," - f"{ch.compensation_enabled},{ch.units}" - ) - - @command("INTYPE") - @split_args() - def intype( - self, - channel, - sensor_type, - auto_range_enabled, - range_, - compensation_enabled, - units, - ): - ch = self.channel_mock[channel] - ch.sensor_type = sensor_type - ch.auto_range_enabled = auto_range_enabled - ch.range = range_ - ch.compensation_enabled = compensation_enabled - ch.units = units - - @query("RANGE?") - def rangeq(self, heater): - h = self.heaters[heater] - return f"{h.output_range}" - - @command("RANGE") - @split_args() - def range_cmd(self, heater, output_range): - h = self.heaters[heater] - h.output_range = output_range - - @query("SETP?") - def setpointq(self, heater): - h = self.heaters[heater] - return f"{h.setpoint}" - - @command("SETP") - @split_args() - def setpoint(self, heater, setpoint): - h = self.heaters[heater] - h.setpoint = setpoint - - @query("TLIMIT?") - def tlimitq(self, channel): - chan = self.channel_mock[channel] - return f"{chan.tlimit}" - - @command("TLIMIT") - @split_args() - def tlimitcmd(self, channel, tlimit): - chan = self.channel_mock[channel] - chan.tlimit = tlimit - - @query("KRDG?") - def temperature(self, output): - chan = self.channel_mock[output] - if self.simulate_heating: - return self.get_t_when_heating() - return f"{chan.T}" - -@instrument_fixture(scope="function") +@pytest.fixture(scope="function") def lakeshore_336(): - return Model_336_Mock( + return Model_336( # type: ignore "lakeshore_336_fixture", "GPIB::2::INSTR", pyvisa_sim_file="lakeshore_model336.yaml", @@ -278,7 +86,6 @@ def test_select_range_limits(lakeshore_336) -> None: def test_set_and_wait_unit_setpoint_reached(lakeshore_336) -> None: ls = lakeshore_336 ls.output_1.setpoint(4) - ls.start_heating() ls.output_1.wait_until_set_point_reached() @@ -287,5 +94,4 @@ def test_blocking_t(lakeshore_336) -> None: h = ls.output_1 ranges = [1.2, 2.4, 3.1] h.range_limits(ranges) - ls.start_heating() h.blocking_t(4) From ac0e8281db6cc4185c317201aac12e0c63f06698 Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Mon, 3 Nov 2025 13:20:41 -0800 Subject: [PATCH 12/19] Replace LakeshoreModel372Mock with LakeshoreModel372 to fix failing test --- tests/test_logger.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_logger.py b/tests/test_logger.py index a148a9657caa..d2762b43be26 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -15,9 +15,9 @@ from qcodes import logger from qcodes.instrument import Instrument from qcodes.instrument_drivers.american_magnetics import AMIModel430, AMIModel4303D +from qcodes.instrument_drivers.Lakeshore import LakeshoreModel372 from qcodes.instrument_drivers.tektronix import TektronixAWG5208 from qcodes.logger.log_analysis import capture_dataframe -from tests.drivers.test_lakeshore_372 import LakeshoreModel372Mock if TYPE_CHECKING: from collections.abc import Callable, Generator @@ -58,8 +58,8 @@ def awg5208(caplog: LogCaptureFixture) -> "Generator[TektronixAWG5208, None, Non @pytest.fixture -def model372() -> "Generator[LakeshoreModel372Mock, None, None]": - inst = LakeshoreModel372Mock( +def model372() -> "Generator[LakeshoreModel372, None, None]": + inst = LakeshoreModel372( "lakeshore_372", "GPIB::3::INSTR", pyvisa_sim_file="lakeshore_model372.yaml", @@ -231,7 +231,7 @@ def test_capture_dataframe() -> None: assert df.message[0] == TEST_LOG_MESSAGE -def test_channels(model372: LakeshoreModel372Mock) -> None: +def test_channels(model372: LakeshoreModel372) -> None: """ Test that messages logged in a channel are propagated to the main instrument. @@ -265,7 +265,7 @@ def test_channels(model372: LakeshoreModel372Mock) -> None: assert f == u -def test_channels_nomessages(model372: LakeshoreModel372Mock) -> None: +def test_channels_nomessages(model372: LakeshoreModel372) -> None: """ Test that messages logged in a channel are not propagated to any instrument. From d6c14207d36f2d0d6d4c5059af8c40da88259496 Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Mon, 3 Nov 2025 13:29:39 -0800 Subject: [PATCH 13/19] Add newsfragment --- docs/changes/newsfragments/7606.improved | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 docs/changes/newsfragments/7606.improved diff --git a/docs/changes/newsfragments/7606.improved b/docs/changes/newsfragments/7606.improved new file mode 100644 index 000000000000..da0fac5bd171 --- /dev/null +++ b/docs/changes/newsfragments/7606.improved @@ -0,0 +1,3 @@ +- Improved pyvisa-sim YAMLs for Lakeshore Models 335, 336, and 372. +- Updated Lakeshore tests to use pyvisa-sim backend instead of mocked classes. +- Updated lakeshore_base.py to bypass waiting when using blocking_t in sim mode. From d27441e5bde20d47ff84c83abad63093f2ecf28c Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Tue, 4 Nov 2025 10:01:59 -0800 Subject: [PATCH 14/19] Use yield in test fixtures for Lakeshore Models 335 and 336 legacy --- tests/drivers/test_lakeshore_335.py | 6 +++++- tests/drivers/test_lakeshore_336_legacy.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/drivers/test_lakeshore_335.py b/tests/drivers/test_lakeshore_335.py index f51f3a987ccd..70f7ac98ae86 100644 --- a/tests/drivers/test_lakeshore_335.py +++ b/tests/drivers/test_lakeshore_335.py @@ -5,12 +5,16 @@ @pytest.fixture(scope="function", name="lakeshore_335") def _make_lakeshore_335(): - return LakeshoreModel335( + inst = LakeshoreModel335( "lakeshore_335_fixture", "GPIB::2::INSTR", pyvisa_sim_file="lakeshore_model335.yaml", device_clear=False, ) + try: + yield inst + finally: + inst.close() def test_pid_set(lakeshore_335) -> None: diff --git a/tests/drivers/test_lakeshore_336_legacy.py b/tests/drivers/test_lakeshore_336_legacy.py index 337f63062c03..2b2ed2e01812 100644 --- a/tests/drivers/test_lakeshore_336_legacy.py +++ b/tests/drivers/test_lakeshore_336_legacy.py @@ -7,12 +7,16 @@ @pytest.fixture(scope="function") def lakeshore_336(): - return Model_336( # type: ignore + inst = Model_336( # type: ignore "lakeshore_336_fixture", "GPIB::2::INSTR", pyvisa_sim_file="lakeshore_model336.yaml", device_clear=False, ) + try: + yield inst + finally: + inst.close() def test_pid_set(lakeshore_336) -> None: From c9679fff83cfc373d23d3a0c8ce4a3cefb39adac Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Tue, 4 Nov 2025 14:32:53 -0800 Subject: [PATCH 15/19] Remove "SIMTEMP" cmd from Lakeshore sim yamls and remove is_simulated check. --- .../instrument/sims/lakeshore_model335.yaml | 4 --- .../instrument/sims/lakeshore_model336.yaml | 8 ----- .../instrument/sims/lakeshore_model372.yaml | 32 ------------------- .../Lakeshore/lakeshore_base.py | 10 ------ 4 files changed, 54 deletions(-) diff --git a/src/qcodes/instrument/sims/lakeshore_model335.yaml b/src/qcodes/instrument/sims/lakeshore_model335.yaml index 9c7755fc8423..07a59516173d 100644 --- a/src/qcodes/instrument/sims/lakeshore_model335.yaml +++ b/src/qcodes/instrument/sims/lakeshore_model335.yaml @@ -16,8 +16,6 @@ devices: getter: q: "KRDG? A" r: "{}" - setter: - q: "SIMTEMP A,{}" # Custom simulation-only command sensor_raw_A: default: 101.0 @@ -77,8 +75,6 @@ devices: getter: q: "KRDG? B" r: "{}" - setter: - q: "SIMTEMP B,{}" # Custom simulation-only command sensor_raw_B: default: 101.0 diff --git a/src/qcodes/instrument/sims/lakeshore_model336.yaml b/src/qcodes/instrument/sims/lakeshore_model336.yaml index dd77fbf24305..fbeca651a23c 100644 --- a/src/qcodes/instrument/sims/lakeshore_model336.yaml +++ b/src/qcodes/instrument/sims/lakeshore_model336.yaml @@ -16,8 +16,6 @@ devices: getter: q: "KRDG? A" r: "{}" - setter: - q: "SIMTEMP A,{}" # Custom simulation-only command sensor_raw_A: default: 101.0 @@ -72,8 +70,6 @@ devices: getter: q: "KRDG? B" r: "{}" - setter: - q: "SIMTEMP B,{}" # Custom simulation-only command sensor_raw_B: default: 101.0 @@ -127,8 +123,6 @@ devices: getter: q: "KRDG? C" r: "{}" - setter: - q: "SIMTEMP C,{}" # Custom simulation-only command sensor_raw_C: default: 101.0 @@ -182,8 +176,6 @@ devices: getter: q: "KRDG? D" r: "{}" - setter: - q: "SIMTEMP D,{}" # Custom simulation-only command sensor_raw_D: default: 101.0 diff --git a/src/qcodes/instrument/sims/lakeshore_model372.yaml b/src/qcodes/instrument/sims/lakeshore_model372.yaml index 5968d68027ba..c3ac32028010 100644 --- a/src/qcodes/instrument/sims/lakeshore_model372.yaml +++ b/src/qcodes/instrument/sims/lakeshore_model372.yaml @@ -23,8 +23,6 @@ devices: getter: q: "KRDG? 1" r: "{}" - setter: - q: "SIMTEMP 1,{}" sensor_raw_1: default: 100.0 @@ -78,8 +76,6 @@ devices: getter: q: "KRDG? 2" r: "{}" - setter: - q: "SIMTEMP 2,{}" sensor_raw_2: default: 100.0 @@ -133,8 +129,6 @@ devices: getter: q: "KRDG? 3" r: "{}" - setter: - q: "SIMTEMP 3,{}" sensor_raw_3: default: 100.0 @@ -188,8 +182,6 @@ devices: getter: q: "KRDG? 4" r: "{}" - setter: - q: "SIMTEMP 4,{}" sensor_raw_4: default: 100.0 @@ -243,8 +235,6 @@ devices: getter: q: "KRDG? 5" r: "{}" - setter: - q: "SIMTEMP 5,{}" sensor_raw_5: default: 100.0 @@ -298,8 +288,6 @@ devices: getter: q: "KRDG? 6" r: "{}" - setter: - q: "SIMTEMP 6,{}" sensor_raw_6: default: 100.0 @@ -353,8 +341,6 @@ devices: getter: q: "KRDG? 7" r: "{}" - setter: - q: "SIMTEMP 7,{}" sensor_raw_7: default: 100.0 @@ -408,8 +394,6 @@ devices: getter: q: "KRDG? 8" r: "{}" - setter: - q: "SIMTEMP 8,{}" sensor_raw_8: default: 100.0 @@ -463,8 +447,6 @@ devices: getter: q: "KRDG? 9" r: "{}" - setter: - q: "SIMTEMP 9,{}" sensor_raw_9: default: 100.0 @@ -518,8 +500,6 @@ devices: getter: q: "KRDG? 10" r: "{}" - setter: - q: "SIMTEMP 10,{}" sensor_raw_10: default: 100.0 @@ -573,8 +553,6 @@ devices: getter: q: "KRDG? 11" r: "{}" - setter: - q: "SIMTEMP 11,{}" sensor_raw_11: default: 100.0 @@ -628,8 +606,6 @@ devices: getter: q: "KRDG? 12" r: "{}" - setter: - q: "SIMTEMP 12,{}" sensor_raw_12: default: 100.0 @@ -683,8 +659,6 @@ devices: getter: q: "KRDG? 13" r: "{}" - setter: - q: "SIMTEMP 13,{}" sensor_raw_13: default: 100.0 @@ -738,8 +712,6 @@ devices: getter: q: "KRDG? 14" r: "{}" - setter: - q: "SIMTEMP 14,{}" sensor_raw_14: default: 100.0 @@ -793,8 +765,6 @@ devices: getter: q: "KRDG? 15" r: "{}" - setter: - q: "SIMTEMP 15,{}" sensor_raw_15: default: 100.0 @@ -848,8 +818,6 @@ devices: getter: q: "KRDG? 16" r: "{}" - setter: - q: "SIMTEMP 16,{}" sensor_raw_16: default: 100.0 diff --git a/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py b/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py index d3a046565bab..3305230337d8 100644 --- a/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py +++ b/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py @@ -373,11 +373,6 @@ def __init__( be reached within the current range. """ - @property - def _is_simulated(self) -> bool: - """Check if this instrument is using PyVISA simulation backend.""" - return getattr(self.root_instrument, "visabackend", None) == "sim" - def _set_blocking_t(self, temperature: float) -> None: self.set_range_from_temperature(temperature) self.setpoint(temperature) @@ -493,11 +488,6 @@ def wait_until_set_point_reached( f"be set to 'kelvin'." ) - if self._is_simulated: - # Use custom SIMTEMP command to update read-only temperature sensor - # in order to "trick" wait loop into thinking temperature was ramped - self.write(f"SIMTEMP {active_channel_name_on_instrument},{self.setpoint()}") - t_setpoint = self.setpoint() time_now = time.perf_counter() From 5aeedacb85e5b1101213a0c2b572d3b1c7924721 Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Tue, 4 Nov 2025 14:51:01 -0800 Subject: [PATCH 16/19] Revert back to using LakeshoreModel372Mock. --- tests/test_logger.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_logger.py b/tests/test_logger.py index d2762b43be26..a148a9657caa 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -15,9 +15,9 @@ from qcodes import logger from qcodes.instrument import Instrument from qcodes.instrument_drivers.american_magnetics import AMIModel430, AMIModel4303D -from qcodes.instrument_drivers.Lakeshore import LakeshoreModel372 from qcodes.instrument_drivers.tektronix import TektronixAWG5208 from qcodes.logger.log_analysis import capture_dataframe +from tests.drivers.test_lakeshore_372 import LakeshoreModel372Mock if TYPE_CHECKING: from collections.abc import Callable, Generator @@ -58,8 +58,8 @@ def awg5208(caplog: LogCaptureFixture) -> "Generator[TektronixAWG5208, None, Non @pytest.fixture -def model372() -> "Generator[LakeshoreModel372, None, None]": - inst = LakeshoreModel372( +def model372() -> "Generator[LakeshoreModel372Mock, None, None]": + inst = LakeshoreModel372Mock( "lakeshore_372", "GPIB::3::INSTR", pyvisa_sim_file="lakeshore_model372.yaml", @@ -231,7 +231,7 @@ def test_capture_dataframe() -> None: assert df.message[0] == TEST_LOG_MESSAGE -def test_channels(model372: LakeshoreModel372) -> None: +def test_channels(model372: LakeshoreModel372Mock) -> None: """ Test that messages logged in a channel are propagated to the main instrument. @@ -265,7 +265,7 @@ def test_channels(model372: LakeshoreModel372) -> None: assert f == u -def test_channels_nomessages(model372: LakeshoreModel372) -> None: +def test_channels_nomessages(model372: LakeshoreModel372Mock) -> None: """ Test that messages logged in a channel are not propagated to any instrument. From 5d027b98be4bc6aa1c38fcf11213f2ee971238a6 Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Tue, 4 Nov 2025 14:52:34 -0800 Subject: [PATCH 17/19] Revert to using mocked Lakeshore class but remove queries and commands covered in new sim yamls. --- tests/drivers/test_lakeshore_335.py | 99 +++++++++- tests/drivers/test_lakeshore_336.py | 128 +++++++++++-- tests/drivers/test_lakeshore_336_legacy.py | 117 +++++++++++- tests/drivers/test_lakeshore_372.py | 204 ++++++++++++++++++++- 4 files changed, 516 insertions(+), 32 deletions(-) diff --git a/tests/drivers/test_lakeshore_335.py b/tests/drivers/test_lakeshore_335.py index 70f7ac98ae86..d676bd15efb1 100644 --- a/tests/drivers/test_lakeshore_335.py +++ b/tests/drivers/test_lakeshore_335.py @@ -1,20 +1,101 @@ -import pytest +import logging +import time +from qcodes.instrument import InstrumentBase from qcodes.instrument_drivers.Lakeshore import LakeshoreModel335 - -@pytest.fixture(scope="function", name="lakeshore_335") +from .test_lakeshore_372 import ( + DictClass, + MockVisaInstrument, + instrument_fixture, + query, +) + +log = logging.getLogger(__name__) + +VISA_LOGGER = ".".join((InstrumentBase.__module__, "com", "visa")) + + +class LakeshoreModel335Mock(MockVisaInstrument, LakeshoreModel335): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + # initial values + self.heaters: dict[str, DictClass] = {} + self.heaters["1"] = DictClass( + P=1, + I=2, + D=3, + mode=1, # 'off' + input_channel=1, # 'A' + powerup_enable=0, + polarity=0, + use_filter=0, + delay=1, + output_range=0, + setpoint=4, + ) + self.heaters["2"] = DictClass( + P=1, + I=2, + D=3, + mode=2, # 'closed_loop' + input_channel=2, # 'B' + powerup_enable=0, + polarity=0, + use_filter=0, + delay=1, + output_range=0, + setpoint=4, + ) + + self.channel_mock = { + str(i): DictClass( + t_limit=i, + T=4, + sensor_name=f"sensor_{i}", + sensor_type=1, # 'diode', + auto_range_enabled=0, # 'off', + range=0, + compensation_enabled=0, # False, + units=1, + ) # 'kelvin') + for i in self.channel_name_command.keys() + } + + # simulate delayed heating + self.simulate_heating = False + self.start_heating_time = time.perf_counter() + + def start_heating(self): + self.start_heating_time = time.perf_counter() + self.simulate_heating = True + + def get_t_when_heating(self): + """ + Simply define a fixed setpoint of 4 k for now + """ + delta = abs(time.perf_counter() - self.start_heating_time) + # make it simple to start with: linear ramp 1K per second + # start at 7K. + return max(4, 7 - delta) + + @query("KRDG?") + def temperature(self, output): + chan = self.channel_mock[output] + if self.simulate_heating: + return self.get_t_when_heating() + return f"{chan.T}" + + +@instrument_fixture(scope="function", name="lakeshore_335") def _make_lakeshore_335(): - inst = LakeshoreModel335( + return LakeshoreModel335Mock( "lakeshore_335_fixture", "GPIB::2::INSTR", pyvisa_sim_file="lakeshore_model335.yaml", device_clear=False, ) - try: - yield inst - finally: - inst.close() def test_pid_set(lakeshore_335) -> None: @@ -88,6 +169,7 @@ def test_select_range_limits(lakeshore_335) -> None: def test_set_and_wait_unit_setpoint_reached(lakeshore_335) -> None: ls = lakeshore_335 ls.output_1.setpoint(4) + ls.start_heating() ls.output_1.wait_until_set_point_reached() @@ -96,4 +178,5 @@ def test_blocking_t(lakeshore_335) -> None: h = ls.output_1 ranges = [1.2, 2.4, 3.1] h.range_limits(ranges) + ls.start_heating() h.blocking_t(4) diff --git a/tests/drivers/test_lakeshore_336.py b/tests/drivers/test_lakeshore_336.py index d9985d963ec0..e7923c1010b7 100644 --- a/tests/drivers/test_lakeshore_336.py +++ b/tests/drivers/test_lakeshore_336.py @@ -1,21 +1,123 @@ +import logging +import time + import pytest +from qcodes.instrument import InstrumentBase from qcodes.instrument_drivers.Lakeshore import LakeshoreModel336 - -@pytest.fixture(scope="function", name="lakeshore_336") +from .test_lakeshore_372 import ( + DictClass, + MockVisaInstrument, + instrument_fixture, + query, +) + +log = logging.getLogger(__name__) + +VISA_LOGGER = ".".join((InstrumentBase.__module__, "com", "visa")) + + +class LakeshoreModel336Mock(MockVisaInstrument, LakeshoreModel336): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + # initial values + self.heaters: dict[str, DictClass] = {} + self.heaters["1"] = DictClass( + P=1, + I=2, + D=3, + mode=1, # 'off' + input_channel=1, # 'A' + powerup_enable=0, + polarity=0, + use_filter=0, + delay=1, + output_range=0, + setpoint=4, + ) + self.heaters["2"] = DictClass( + P=1, + I=2, + D=3, + mode=2, # 'closed_loop' + input_channel=2, # 'B' + powerup_enable=0, + polarity=0, + use_filter=0, + delay=1, + output_range=0, + setpoint=4, + ) + self.heaters["3"] = DictClass( + mode=4, # 'monitor_out' + input_channel=2, # 'B' + powerup_enable=0, + polarity=0, + use_filter=0, + delay=1, + output_range=0, + setpoint=4, + ) + self.heaters["4"] = DictClass( + mode=5, # 'warm_up' + input_channel=1, # 'A' + powerup_enable=0, + polarity=0, + use_filter=0, + delay=1, + output_range=0, + setpoint=4, + ) + + self.channel_mock = { + str(i): DictClass( + t_limit=i, + T=4, + sensor_name=f"sensor_{i}", + sensor_type=1, # 'diode', + auto_range_enabled=0, # 'off', + range=0, + compensation_enabled=0, # False, + units=1, # 'kelvin' + ) + for i in self.channel_name_command.keys() + } + + # simulate delayed heating + self.simulate_heating = False + self.start_heating_time = time.perf_counter() + + def start_heating(self): + self.start_heating_time = time.perf_counter() + self.simulate_heating = True + + def get_t_when_heating(self): + """ + Simply define a fixed setpoint of 4 k for now + """ + delta = abs(time.perf_counter() - self.start_heating_time) + # make it simple to start with: linear ramp 1K per second + # start at 7K. + return max(4, 7 - delta) + + @query("KRDG?") + def temperature(self, output): + chan = self.channel_mock[output] + if self.simulate_heating: + return self.get_t_when_heating() + return f"{chan.T}" + + +@instrument_fixture(scope="function", name="lakeshore_336") def _make_lakeshore_336(): - """Create a Lakeshore 336 instance using PyVISA-sim backend.""" - inst = LakeshoreModel336( - "lakeshore_336", + return LakeshoreModel336Mock( + "lakeshore_336_fixture", "GPIB::2::INSTR", pyvisa_sim_file="lakeshore_model336.yaml", device_clear=False, ) - try: - yield inst - finally: - inst.close() def test_pid_set(lakeshore_336) -> None: @@ -100,20 +202,16 @@ def test_select_range_limits(lakeshore_336) -> None: def test_set_and_wait_unit_setpoint_reached(lakeshore_336) -> None: - """Test that wait_until_set_point_reached completes in simulation mode.""" ls = lakeshore_336 ls.output_1.setpoint(4) - # In simulation mode, wait_until_set_point_reached should return immediately - # because _is_simulated check bypasses the wait loop + ls.start_heating() ls.output_1.wait_until_set_point_reached() def test_blocking_t(lakeshore_336) -> None: - """Test that blocking_t completes in simulation mode.""" ls = lakeshore_336 h = ls.output_1 ranges = [1.2, 2.4, 3.1] h.range_limits(ranges) - # In simulation mode, blocking_t should return immediately - # because _is_simulated check bypasses the wait loop + ls.start_heating() h.blocking_t(4) diff --git a/tests/drivers/test_lakeshore_336_legacy.py b/tests/drivers/test_lakeshore_336_legacy.py index 2b2ed2e01812..adef5b5e6bd6 100644 --- a/tests/drivers/test_lakeshore_336_legacy.py +++ b/tests/drivers/test_lakeshore_336_legacy.py @@ -1,22 +1,123 @@ -import pytest +import logging +import time +from qcodes.instrument import InstrumentBase from qcodes.instrument_drivers.Lakeshore.Model_336 import ( Model_336, # pyright: ignore[reportDeprecated] ) +from .test_lakeshore_372 import ( + DictClass, + MockVisaInstrument, + instrument_fixture, + query, +) -@pytest.fixture(scope="function") +log = logging.getLogger(__name__) + +VISA_LOGGER = ".".join((InstrumentBase.__module__, "com", "visa")) + + +class Model_336_Mock(MockVisaInstrument, Model_336): # pyright: ignore[reportDeprecated] + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + # initial values + self.heaters: dict[str, DictClass] = {} + self.heaters["1"] = DictClass( + P=1, + I=2, + D=3, + mode=1, # 'off' + input_channel=1, # 'A' + powerup_enable=0, + polarity=0, + use_filter=0, + delay=1, + output_range=0, + setpoint=4, + ) + self.heaters["2"] = DictClass( + P=1, + I=2, + D=3, + mode=2, # 'closed_loop' + input_channel=2, # 'B' + powerup_enable=0, + polarity=0, + use_filter=0, + delay=1, + output_range=0, + setpoint=4, + ) + self.heaters["3"] = DictClass( + mode=4, # 'monitor_out' + input_channel=2, # 'B' + powerup_enable=0, + polarity=0, + use_filter=0, + delay=1, + output_range=0, + setpoint=4, + ) + self.heaters["4"] = DictClass( + mode=5, # 'warm_up' + input_channel=1, # 'A' + powerup_enable=0, + polarity=0, + use_filter=0, + delay=1, + output_range=0, + setpoint=4, + ) + + self.channel_mock = { + str(i): DictClass( + t_limit=i, + T=4, + sensor_name=f"sensor_{i}", + sensor_type=1, # 'diode', + auto_range_enabled=0, # 'off', + range=0, + compensation_enabled=0, # False, + units=1, + ) # 'kelvin') + for i in self.channel_name_command.keys() + } + + # simulate delayed heating + self.simulate_heating = False + self.start_heating_time = time.perf_counter() + + def start_heating(self): + self.start_heating_time = time.perf_counter() + self.simulate_heating = True + + def get_t_when_heating(self): + """ + Simply define a fixed setpoint of 4 k for now + """ + delta = abs(time.perf_counter() - self.start_heating_time) + # make it simple to start with: linear ramp 1K per second + # start at 7K. + return max(4, 7 - delta) + + @query("KRDG?") + def temperature(self, output): + chan = self.channel_mock[output] + if self.simulate_heating: + return self.get_t_when_heating() + return f"{chan.T}" + + +@instrument_fixture(scope="function") def lakeshore_336(): - inst = Model_336( # type: ignore + return Model_336_Mock( "lakeshore_336_fixture", "GPIB::2::INSTR", pyvisa_sim_file="lakeshore_model336.yaml", device_clear=False, ) - try: - yield inst - finally: - inst.close() def test_pid_set(lakeshore_336) -> None: @@ -90,6 +191,7 @@ def test_select_range_limits(lakeshore_336) -> None: def test_set_and_wait_unit_setpoint_reached(lakeshore_336) -> None: ls = lakeshore_336 ls.output_1.setpoint(4) + ls.start_heating() ls.output_1.wait_until_set_point_reached() @@ -98,4 +200,5 @@ def test_blocking_t(lakeshore_336) -> None: h = ls.output_1 ranges = [1.2, 2.4, 3.1] h.range_limits(ranges) + ls.start_heating() h.blocking_t(4) diff --git a/tests/drivers/test_lakeshore_372.py b/tests/drivers/test_lakeshore_372.py index 03a75008c775..1ff25916e082 100644 --- a/tests/drivers/test_lakeshore_372.py +++ b/tests/drivers/test_lakeshore_372.py @@ -1,18 +1,215 @@ from __future__ import annotations -from typing import Literal, TypeVar +import logging +import time +import warnings +from contextlib import suppress +from typing import TYPE_CHECKING, Any, Literal, TypeVar import pytest from typing_extensions import ParamSpec +from qcodes.instrument import InstrumentBase from qcodes.instrument_drivers.Lakeshore import LakeshoreModel372 from qcodes.instrument_drivers.Lakeshore.lakeshore_base import ( LakeshoreBaseSensorChannel, ) +from qcodes.logger import get_instrument_logger +from qcodes.utils import QCoDeSDeprecationWarning + +if TYPE_CHECKING: + from collections.abc import Callable + +log = logging.getLogger(__name__) + +VISA_LOGGER = ".".join((InstrumentBase.__module__, "com", "visa")) P = ParamSpec("P") T = TypeVar("T") +P = ParamSpec("P") +T = TypeVar("T") + + +class MockVisaInstrument: + """ + Mixin class that overrides write_raw and ask_raw to simulate an + instrument. + """ + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.visa_log = get_instrument_logger(self, VISA_LOGGER) # type: ignore[arg-type] + + # This base class mixin holds two dictionaries associated with the + # pyvisa_instrument.write() + self.cmds: dict[str, Callable[..., Any]] = {} + # and pyvisa_instrument.query() functions + self.queries: dict[str, Callable[..., Any]] = {} + # the keys are the issued VISA commands like '*IDN?' or '*OPC' + # the values are the corresponding methods to be called on the mock + # instrument. + + # To facilitate the definition there are the decorators `@query' and + # `@command`. These attach an attribute to the method, so that the + # dictionaries can be filled here in the constructor. (This is + # borderline abusive, but makes a it easy to define mocks) + func_names = dir(self) + # cycle through all methods + for func_name in func_names: + with warnings.catch_warnings(): + if func_name == "_name": + # silence warning when getting deprecated attribute + warnings.simplefilter("ignore", category=QCoDeSDeprecationWarning) + + f = getattr(self, func_name) + # only add for methods that have such an attribute + with suppress(AttributeError): + self.queries[getattr(f, "query_name")] = f + with suppress(AttributeError): + self.cmds[getattr(f, "command_name")] = f + + def write_raw(self, cmd) -> None: + cmd_parts = cmd.split(" ") + cmd_str = cmd_parts[0].upper() + if cmd_str in self.cmds: + args = "".join(cmd_parts[1:]) + self.visa_log.debug(f"Query: {cmd} for command {cmd_str} with args {args}") + self.cmds[cmd_str](args) + else: + super().write_raw(cmd) # type: ignore[misc] + + def ask_raw(self, cmd) -> Any: + query_parts = cmd.split(" ") + query_str = query_parts[0].upper() + if query_str in self.queries: + args = "".join(query_parts[1:]) + self.visa_log.debug( + f"Query: {cmd} for command {query_str} with args {args}" + ) + response = self.queries[query_str](args) + self.visa_log.debug(f"Response: {response}") + return response + else: + return super().ask_raw(cmd) # type: ignore[misc] + + +def query(name: str) -> Callable[[Callable[P, T]], Callable[P, T]]: + def wrapper(func: Callable[P, T]) -> Callable[P, T]: + func.query_name = name.upper() # type: ignore[attr-defined] + return func + + return wrapper + + +def command(name: str) -> Callable[[Callable[P, T]], Callable[P, T]]: + def wrapper(func: Callable[P, T]) -> Callable[P, T]: + func.command_name = name.upper() # type: ignore[attr-defined] + return func + + return wrapper + + +class DictClass: + def __init__(self, **kwargs): + # https://stackoverflow.com/questions/16237659/python-how-to-implement-getattr + super().__setattr__("_attrs", kwargs) + + for kwarg, value in kwargs.items(): + self._attrs[kwarg] = value + + def __getattr__(self, attr): + try: + return self._attrs[attr] + except KeyError as e: + raise AttributeError from e + + def __setattr__(self, name: str, value: Any) -> None: + self._attrs[name] = value + + +class LakeshoreModel372Mock(MockVisaInstrument, LakeshoreModel372): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + # initial values + self.heaters: dict[str, DictClass] = {} + self.heaters["0"] = DictClass( + P=1, + I=2, + D=3, + mode=5, + input_channel=2, + powerup_enable=0, + polarity=0, + use_filter=0, + delay=1, + output_range=0, + setpoint=4, + ) + self.heaters["1"] = DictClass( + P=1, + I=2, + D=3, + mode=5, + input_channel=2, + powerup_enable=0, + polarity=0, + use_filter=0, + delay=1, + output_range=0, + setpoint=4, + ) + self.heaters["2"] = DictClass( + P=1, + I=2, + D=3, + mode=5, + input_channel=2, + powerup_enable=0, + polarity=0, + use_filter=0, + delay=1, + output_range=0, + setpoint=4, + ) + + self.channel_mock = { + str(i): DictClass( + tlimit=i, + T=4, + enabled=1, # True + dwell=100, + pause=3, + curve_number=0, + temperature_coefficient=1, # 'negative', + excitation_mode=0, #'voltage', + excitation_range_number=1, + auto_range=0, #'off', + range=5, #'200 mOhm', + current_source_shunted=0, # False, + units=1, + ) #'kelvin') + for i in range(1, 17) + } + + # simulate delayed heating + self.simulate_heating = False + self.start_heating_time = time.perf_counter() + + def start_heating(self): + self.start_heating_time = time.perf_counter() + self.simulate_heating = True + + def get_t_when_heating(self): + """ + Simply define a fixed setpoint of 4 k for now + """ + delta = abs(time.perf_counter() - self.start_heating_time) + # make it simple to start with: linear ramp 1K per second + # start at 7K. + return max(4, 7 - delta) + def instrument_fixture( scope: Literal["session", "package", "module", "class", "function"] = "function", @@ -34,7 +231,7 @@ def wrapped_fixture(): @instrument_fixture(scope="function") def lakeshore_372(): - return LakeshoreModel372( + return LakeshoreModel372Mock( "lakeshore_372_fixture", "GPIB::3::INSTR", pyvisa_sim_file="lakeshore_model372.yaml", @@ -115,13 +312,16 @@ def test_select_range_limits(lakeshore_372) -> None: def test_set_and_wait_unit_setpoint_reached(lakeshore_372) -> None: ls = lakeshore_372 ls.sample_heater.setpoint(4) + ls.start_heating() ls.sample_heater.wait_until_set_point_reached() def test_blocking_t(lakeshore_372) -> None: + ls = lakeshore_372 h = lakeshore_372.sample_heater ranges = list(range(1, 9)) h.range_limits(ranges) + ls.start_heating() h.blocking_t(4) From 953c5a60d7107133d08658428dd51cf6e255f879 Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Thu, 6 Nov 2025 07:46:19 -0800 Subject: [PATCH 18/19] Remove duplicate variables. --- tests/drivers/test_lakeshore_372.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/drivers/test_lakeshore_372.py b/tests/drivers/test_lakeshore_372.py index 1ff25916e082..caff629fe192 100644 --- a/tests/drivers/test_lakeshore_372.py +++ b/tests/drivers/test_lakeshore_372.py @@ -27,9 +27,6 @@ P = ParamSpec("P") T = TypeVar("T") -P = ParamSpec("P") -T = TypeVar("T") - class MockVisaInstrument: """ From f05643b889700aed42ebea9d9962ca657b639043 Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Thu, 6 Nov 2025 07:49:12 -0800 Subject: [PATCH 19/19] Add missing KRDG? query. --- tests/drivers/test_lakeshore_372.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/drivers/test_lakeshore_372.py b/tests/drivers/test_lakeshore_372.py index caff629fe192..4ebe96f40f49 100644 --- a/tests/drivers/test_lakeshore_372.py +++ b/tests/drivers/test_lakeshore_372.py @@ -207,6 +207,13 @@ def get_t_when_heating(self): # start at 7K. return max(4, 7 - delta) + @query("KRDG?") + def temperature(self, output): + chan = self.channel_mock[output] + if self.simulate_heating: + return self.get_t_when_heating() + return f"{chan.T}" + def instrument_fixture( scope: Literal["session", "package", "module", "class", "function"] = "function",