From 0e3fa1cf8cf5b11d8e893b1f3f2abc749a051eb9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 04:02:26 +0000 Subject: [PATCH 01/53] Bump bleach from 6.2.0 to 6.3.0 Bumps [bleach](https://github.com/mozilla/bleach) from 6.2.0 to 6.3.0. - [Changelog](https://github.com/mozilla/bleach/blob/main/CHANGES) - [Commits](https://github.com/mozilla/bleach/compare/v6.2.0...v6.3.0) --- updated-dependencies: - dependency-name: bleach dependency-version: 6.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 49ff8ed0968..0d226475075 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,7 @@ beautifulsoup4==4.14.2 # via # furo # nbconvert -bleach==6.2.0 +bleach==6.3.0 # via nbconvert broadbean==0.14.0 # via From 04815b73d4932fc3570061999d68cc1197ec00de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 04:02:54 +0000 Subject: [PATCH 02/53] Bump ipykernel from 7.0.1 to 7.1.0 Bumps [ipykernel](https://github.com/ipython/ipykernel) from 7.0.1 to 7.1.0. - [Release notes](https://github.com/ipython/ipykernel/releases) - [Changelog](https://github.com/ipython/ipykernel/blob/main/CHANGELOG.md) - [Commits](https://github.com/ipython/ipykernel/compare/v7.0.1...v7.1.0) --- updated-dependencies: - dependency-name: ipykernel dependency-version: 7.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 49ff8ed0968..3f71290a07a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -112,7 +112,7 @@ importlib-metadata==8.7.0 # via opentelemetry-api iniconfig==2.3.0 # via pytest -ipykernel==7.0.1 +ipykernel==7.1.0 # via # qcodes (pyproject.toml) # qcodes From 980be7f6ab00e5fc3ac10137932243ba340bc62e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 04:02:40 +0000 Subject: [PATCH 03/53] Bump scipy from 1.16.2 to 1.16.3 Bumps [scipy](https://github.com/scipy/scipy) from 1.16.2 to 1.16.3. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.16.2...v1.16.3) --- updated-dependencies: - dependency-name: scipy dependency-version: 1.16.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f0616e2ff08..a486d585b84 100644 --- a/requirements.txt +++ b/requirements.txt @@ -371,7 +371,7 @@ ruamel-yaml-clib==0.2.14 # via ruamel-yaml schema==0.7.8 # via broadbean -scipy==1.16.2 +scipy==1.16.3 # via qcodes (pyproject.toml) six==1.17.0 # via python-dateutil From 67c3110f12de23a91b9f29324bf3e3f7603e3db7 Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Wed, 29 Oct 2025 17:17:02 -0700 Subject: [PATCH 04/53] 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 1e0277c31b1..7da9c06b1c9 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 05/53] 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 3305230337d..a36dcbc7483 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 346bd87517a3f76d09f4f64d182632bd69700d33 Mon Sep 17 00:00:00 2001 From: Samantha Ho Date: Thu, 30 Oct 2025 11:41:28 -0700 Subject: [PATCH 06/53] Bugfix: Nested LinSweepers --- src/qcodes/dataset/measurement_extensions.py | 1 + tests/dataset/test_measurement_extensions.py | 25 ++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/qcodes/dataset/measurement_extensions.py b/src/qcodes/dataset/measurement_extensions.py index 3634ebb062c..9472d27a28d 100644 --- a/src/qcodes/dataset/measurement_extensions.py +++ b/src/qcodes/dataset/measurement_extensions.py @@ -236,4 +236,5 @@ def __next__(self) -> float: self._iter_index += 1 return set_val else: + self._iter_index = 0 raise StopIteration diff --git a/tests/dataset/test_measurement_extensions.py b/tests/dataset/test_measurement_extensions.py index 34fd0e7abe3..87a342b7323 100644 --- a/tests/dataset/test_measurement_extensions.py +++ b/tests/dataset/test_measurement_extensions.py @@ -1,4 +1,5 @@ import gc +import itertools from functools import partial from itertools import product from pathlib import Path @@ -238,6 +239,30 @@ def test_linsweeper(default_params, default_database_and_experiment): ) +def test_nested_linsweeper(default_params): + set1, set2, _, _, _, _ = default_params + linsweeper1 = LinSweeper(set1, 0, 1, 11, 0.001) + linsweeper2 = LinSweeper(set2, -1, 0, 6, 0.001) + data_pairs = [] + for _ in linsweeper1: + for _ in linsweeper2: + data_pairs.append([set1(), set2()]) + + assert len(data_pairs) == 11 * 6 + assert np.all( + np.isclose( + np.array(data_pairs), + np.array( + list( + itertools.product( + linsweeper1.get_setpoints(), linsweeper2.get_setpoints() + ) + ) + ), + ) + ) + + def test_context_with_pws(pws_params, default_database_and_experiment): _ = default_database_and_experiment pws1, set1 = pws_params From 0ffea17ec3f57711f1ff7fd98ffc7aac9017001b Mon Sep 17 00:00:00 2001 From: Samantha Ho Date: Thu, 30 Oct 2025 11:50:38 -0700 Subject: [PATCH 07/53] Add newsfragment --- docs/changes/newsfragments/7607.improved | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 docs/changes/newsfragments/7607.improved diff --git a/docs/changes/newsfragments/7607.improved b/docs/changes/newsfragments/7607.improved new file mode 100644 index 00000000000..b6ad1438466 --- /dev/null +++ b/docs/changes/newsfragments/7607.improved @@ -0,0 +1,2 @@ +Fixes a bug in the LinSweeper iterator that caused it to always raise StopIteration after +completing a single sweep. This bug meant LinSweeper could not be used in a nested measurement function. From f55151b5c0c0fc99bcf9bd8d56eac8bb90205ff9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 04:01:37 +0000 Subject: [PATCH 08/53] Bump the zhinst group with 2 updates Bumps the zhinst group with 2 updates: zhinst-core and zhinst-timing-models. Updates `zhinst-core` from 25.7.0.507 to 25.10.0.274 Updates `zhinst-timing-models` from 25.7.0 to 25.10.0 --- updated-dependencies: - dependency-name: zhinst-core dependency-version: 25.10.0.274 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: zhinst - dependency-name: zhinst-timing-models dependency-version: 25.10.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: zhinst ... Signed-off-by: dependabot[bot] --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index a486d585b84..d9eda7a0e71 100644 --- a/requirements.txt +++ b/requirements.txt @@ -514,13 +514,13 @@ xarray==2025.10.1 # cf-xarray # qcodes # qcodes-loop -zhinst-core==25.7.0.507 +zhinst-core==25.10.0.274 # via # zhinst-toolkit # zhinst-utils zhinst-qcodes==0.7.0 # via qcodes (pyproject.toml) -zhinst-timing-models==25.7.0 +zhinst-timing-models==25.10.0 # via zhinst-utils zhinst-toolkit==1.1.0 # via zhinst-qcodes From d431e4d048e872510f42135372f391c7ed939326 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 04:01:53 +0000 Subject: [PATCH 09/53] Bump github/codeql-action from 4.31.0 to 4.31.2 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.0 to 4.31.2. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/4e94bd11f71e507f7f87df81788dff88d1dacbfb...0499de31b99561a6d14a36a5f662c2a54f91beee) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.31.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8a1cb631e38..d2ba849b849 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,12 +39,12 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v3.29.5 + uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v3.29.5 with: languages: ${{ matrix.language }} - name: Autobuild - uses: github/codeql-action/autobuild@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v3.29.5 + uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee # v3.29.5 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v3.29.5 + uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v3.29.5 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index b85162f77ef..23d8dbe13dd 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -71,6 +71,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v3.29.5 + uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v3.29.5 with: sarif_file: results.sarif From 4bc640690a3623888ce1f55df98cdef72f17a22a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 04:02:54 +0000 Subject: [PATCH 10/53] Bump fsspec from 2025.9.0 to 2025.10.0 Bumps [fsspec](https://github.com/fsspec/filesystem_spec) from 2025.9.0 to 2025.10.0. - [Commits](https://github.com/fsspec/filesystem_spec/compare/2025.9.0...2025.10.0) --- updated-dependencies: - dependency-name: fsspec dependency-version: 2025.10.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a486d585b84..7b67b02852e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -85,7 +85,7 @@ fastjsonschema==2.21.2 # via nbformat fonttools==4.60.1 # via matplotlib -fsspec==2025.9.0 +fsspec==2025.10.0 # via dask furo==2025.9.25 # via qcodes (pyproject.toml) From 8392a50fb78eb4c31b4630763d71af7728a6acae Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 31 Oct 2025 08:02:04 +0100 Subject: [PATCH 11/53] Add pythonm 3.14 support --- .github/workflows/docs.yaml | 4 +++- .github/workflows/pytest.yaml | 4 +++- pyproject.toml | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 838410795c0..f829612a2a5 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -29,12 +29,14 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] - python-version: ["3.11", "3.12", "3.13"] + python-version: ["3.11", "3.12", "3.13", "3.14"] exclude: - os: windows-latest python-version: 3.11 - os: windows-latest python-version: 3.13 + - os: windows-latest + python-version: 3.14 env: OS: ${{ matrix.os }} SPHINX_WARNINGS_AS_ERROR: true diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index e21eb3ef4b7..b29bb99717d 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -30,7 +30,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] - python-version: ["3.11", "3.12", "3.13"] + python-version: ["3.11", "3.12", "3.13", "3.14"] min-version: [false] include: - os: ubuntu-latest @@ -43,6 +43,8 @@ jobs: python-version: "3.11" - os: windows-latest python-version: "3.13" + - os: windows-latest + python-version: "3.14" env: OS: ${{ matrix.os }} PYTHON: ${{ matrix.python-version }} diff --git a/pyproject.toml b/pyproject.toml index 8f67137a7cd..53e3fe9eff6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering", ] license = "MIT" @@ -217,11 +218,10 @@ markers = "serial" filterwarnings = [ 'error', 'ignore:open_binary is deprecated:DeprecationWarning', # pyvisa-sim deprecated in 3.11 un-deprecated in 3.12. Drop filter once we drop support for 3.11 - 'ignore:Jupyter is migrating its paths to use standard platformdirs:DeprecationWarning', # jupyter 'ignore:unclosed database in:ResourceWarning', # internal should be fixed 'ignore:unclosed\ Date: Fri, 31 Oct 2025 08:05:33 +0100 Subject: [PATCH 12/53] upgrade ruff to latest version --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index be836b1fbd2..1554b1101e5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: 'v0.14.1' + rev: 'v0.14.3' hooks: - id: ruff-check types_or: [python, pyi, jupyter, toml] From 3544760c4e15c606102004a040b9e533814f9223 Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Fri, 31 Oct 2025 10:27:58 -0700 Subject: [PATCH 13/53] 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 7da9c06b1c9..dd77fbf2430 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 14/53] 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 a36dcbc7483..d3a046565ba 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 15/53] 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 977e04adec0..509c087608e 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 16/53] 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 0ba20e47b29..d9985d963ec 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 17/53] 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 050c952da2e..5968d68027b 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 18/53] 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 4f59e3f3d1a..03a75008c77 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 028fb5dde0ba849cb7e4c5815d4def4627ba28be Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 1 Nov 2025 10:21:04 +0100 Subject: [PATCH 19/53] Tektronix DPO7200xx assign modules to types --- .../instrument_drivers/tektronix/DPO7200xx.py | 16 ++++++++++++---- tests/drivers/test_tektronix_dpo7200xx.py | 12 ++++++++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/qcodes/instrument_drivers/tektronix/DPO7200xx.py b/src/qcodes/instrument_drivers/tektronix/DPO7200xx.py index dc9d64e9cd5..57929e2c0c5 100644 --- a/src/qcodes/instrument_drivers/tektronix/DPO7200xx.py +++ b/src/qcodes/instrument_drivers/tektronix/DPO7200xx.py @@ -106,10 +106,14 @@ def __init__( self.add_submodule(measurement_name, measurement_module) measurement_list.append(measurement_module) - self.add_submodule("measurement", measurement_list) - self.add_submodule( + self.measurement: ChannelList[TektronixDPOMeasurement] = self.add_submodule( + "measurement", measurement_list + ) + """Instrument module measurement""" + self.statistics: TektronixDPOMeasurementStatistics = self.add_submodule( "statistics", TektronixDPOMeasurementStatistics(self, "statistics") ) + """Instrument module statistics""" channel_list = ChannelList(self, "channel", TektronixDPOChannel) for channel_number in range(1, self.number_of_channels + 1): @@ -123,7 +127,10 @@ def __init__( self.add_submodule(channel_name, channel_module) channel_list.append(channel_module) - self.add_submodule("channel", channel_list) + self.channel: ChannelList[TektronixDPOChannel] = self.add_submodule( + "channel", channel_list + ) + """Instrument module channel""" self.connect_message() @@ -445,9 +452,10 @@ def __init__( super().__init__(parent, name, **kwargs) self._identifier = f"CH{channel_number}" - self.add_submodule( + self.waveform: TektronixDPOWaveform = self.add_submodule( "waveform", TektronixDPOWaveform(self, "waveform", self._identifier) ) + """Instrument module waveform""" self.scale: Parameter = self.add_parameter( "scale", diff --git a/tests/drivers/test_tektronix_dpo7200xx.py b/tests/drivers/test_tektronix_dpo7200xx.py index 016ff6beaf6..8d6eef682d5 100644 --- a/tests/drivers/test_tektronix_dpo7200xx.py +++ b/tests/drivers/test_tektronix_dpo7200xx.py @@ -1,13 +1,17 @@ import sys import timeit +from typing import TYPE_CHECKING import pytest from qcodes.instrument_drivers.tektronix.DPO7200xx import TektronixDPO7000xx +if TYPE_CHECKING: + from collections.abc import Generator + @pytest.fixture(scope="function") -def tektronix_dpo(): +def tektronix_dpo() -> "Generator[TektronixDPO7000xx, None, None]": """ A six channel-per-relay instrument """ @@ -24,7 +28,7 @@ def tektronix_dpo(): @pytest.mark.xfail( condition=sys.platform == "win32", reason="Time resolution is too low on windows" ) -def test_adjust_timer(tektronix_dpo) -> None: +def test_adjust_timer(tektronix_dpo: TektronixDPO7000xx) -> None: """ After adjusting the type of the measurement or the source of the measurement, we need wait at least 0.1 seconds @@ -54,7 +58,7 @@ def test_adjust_timer(tektronix_dpo) -> None: # measurements slightly sooner then 'minimum_adjustment_time' -def test_measurements_return_float(tektronix_dpo) -> None: +def test_measurements_return_float(tektronix_dpo: TektronixDPO7000xx) -> None: amplitude = tektronix_dpo.measurement[0].amplitude() assert isinstance(amplitude, float) @@ -62,6 +66,6 @@ def test_measurements_return_float(tektronix_dpo) -> None: assert isinstance(mean_amplitude, float) -def test_measurement_sets_state(tektronix_dpo) -> None: +def test_measurement_sets_state(tektronix_dpo: TektronixDPO7000xx) -> None: tektronix_dpo.measurement[1].frequency() assert tektronix_dpo.measurement[1].state() == 1 From 0c656f3dd0440b261fd74e8b5b4e6384436fdbc3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 04:02:19 +0000 Subject: [PATCH 20/53] Bump the ipywidgets group with 3 updates Bumps the ipywidgets group with 3 updates: [ipywidgets](https://github.com/jupyter-widgets/ipywidgets), [jupyterlab-widgets](https://github.com/jupyter-widgets/ipywidgets) and [widgetsnbextension](http://jupyter.org). Updates `ipywidgets` from 8.1.7 to 8.1.8 - [Release notes](https://github.com/jupyter-widgets/ipywidgets/releases) - [Commits](https://github.com/jupyter-widgets/ipywidgets/compare/8.1.7...8.1.8) Updates `jupyterlab-widgets` from 3.0.15 to 3.0.16 - [Release notes](https://github.com/jupyter-widgets/ipywidgets/releases) - [Commits](https://github.com/jupyter-widgets/ipywidgets/commits) Updates `widgetsnbextension` from 4.0.14 to 4.0.15 --- updated-dependencies: - dependency-name: ipywidgets dependency-version: 8.1.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: ipywidgets - dependency-name: jupyterlab-widgets dependency-version: 3.0.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: ipywidgets - dependency-name: widgetsnbextension dependency-version: 4.0.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: ipywidgets ... Signed-off-by: dependabot[bot] --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4f6e978897d..ccffa615388 100644 --- a/requirements.txt +++ b/requirements.txt @@ -123,7 +123,7 @@ ipython==9.6.0 # qcodes-loop ipython-pygments-lexers==1.1.1 # via ipython -ipywidgets==8.1.7 +ipywidgets==8.1.8 # via # qcodes (pyproject.toml) # qcodes @@ -160,7 +160,7 @@ jupyter-core==5.9.1 # nbformat jupyterlab-pygments==0.3.0 # via nbconvert -jupyterlab-widgets==3.0.15 +jupyterlab-widgets==3.0.16 # via ipywidgets kiwisolver==1.4.9 # via matplotlib @@ -506,7 +506,7 @@ websockets==15.0.1 # via # qcodes (pyproject.toml) # qcodes -widgetsnbextension==4.0.14 +widgetsnbextension==4.0.15 # via ipywidgets xarray==2025.10.1 # via From a04aae363f16b2a9006cc91d5595439786fc127d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 04:02:50 +0000 Subject: [PATCH 21/53] Bump psutil from 7.1.2 to 7.1.3 Bumps [psutil](https://github.com/giampaolo/psutil) from 7.1.2 to 7.1.3. - [Changelog](https://github.com/giampaolo/psutil/blob/master/HISTORY.rst) - [Commits](https://github.com/giampaolo/psutil/compare/release-7.1.2...release-7.1.3) --- updated-dependencies: - dependency-name: psutil dependency-version: 7.1.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4f6e978897d..016fa9ac471 100644 --- a/requirements.txt +++ b/requirements.txt @@ -281,7 +281,7 @@ pluggy==1.6.0 # pytest-cov prompt-toolkit==3.0.52 # via ipython -psutil==7.1.2 +psutil==7.1.3 # via ipykernel pure-eval==0.2.3 # via stack-data From 054a9ab7f44c3e2988352184ac2bbc5c18ffbc5e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 04:03:21 +0000 Subject: [PATCH 22/53] Bump hypothesis from 6.142.4 to 6.144.0 Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.142.4 to 6.144.0. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.142.4...hypothesis-python-6.144.0) --- updated-dependencies: - dependency-name: hypothesis dependency-version: 6.144.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4f6e978897d..241e74ced6e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -102,7 +102,7 @@ h5py==3.15.1 # qcodes-loop hickle==5.0.3 # via qcodes-loop -hypothesis==6.142.4 +hypothesis==6.144.0 # via qcodes (pyproject.toml) idna==3.11 # via requests From e37989f9ddf2925357197a13f2a8cfc332e7716a Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 3 Nov 2025 11:16:40 +0100 Subject: [PATCH 23/53] Use Mapping rather than dict in DataSetDefinition Allows any dict like class to be used and signal to users and type checkers that the input will not be modified. --- src/qcodes/dataset/measurement_extensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qcodes/dataset/measurement_extensions.py b/src/qcodes/dataset/measurement_extensions.py index 9472d27a28d..719c6695fa0 100644 --- a/src/qcodes/dataset/measurement_extensions.py +++ b/src/qcodes/dataset/measurement_extensions.py @@ -1,7 +1,7 @@ from __future__ import annotations import time -from collections.abc import Generator, Sequence +from collections.abc import Generator, Mapping, Sequence from contextlib import ExitStack, contextmanager from dataclasses import dataclass from typing import TYPE_CHECKING, Any @@ -37,7 +37,7 @@ class DataSetDefinition: experiment: Experiment | None = None """An optional argument specifying which Experiment this dataset should be written to""" - metadata: dict[str, Any] | None = None + metadata: Mapping[str, Any] | None = None """An optional dictionary of metadata that will be added to the dataset generated by this definition""" From 590c31f2c0121fad2292bca2d6d648ade7687e0b Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Mon, 3 Nov 2025 12:00:36 -0800 Subject: [PATCH 24/53] 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 1166b1560fc..9c7755fc842 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 25/53] 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 0120707befa..f51f3a987cc 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 26/53] 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 3501ce2b3f5..337f63062c0 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 27/53] 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 a148a9657ca..d2762b43be2 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 28/53] 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 00000000000..da0fac5bd17 --- /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 169c47a49a8257e55852b6b3470e9c905f157992 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 04:01:35 +0000 Subject: [PATCH 29/53] Bump zhinst-toolkit from 1.1.0 to 1.2.0 in the zhinst group Bumps the zhinst group with 1 update: [zhinst-toolkit](https://github.com/zhinst/zhinst-toolkit). Updates `zhinst-toolkit` from 1.1.0 to 1.2.0 - [Changelog](https://github.com/zhinst/zhinst-toolkit/blob/main/CHANGELOG.md) - [Commits](https://github.com/zhinst/zhinst-toolkit/compare/v1.1.0...v1.2.0) --- updated-dependencies: - dependency-name: zhinst-toolkit dependency-version: 1.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: zhinst ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fc11f78edf1..9d3953690de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -522,7 +522,7 @@ zhinst-qcodes==0.7.0 # via qcodes (pyproject.toml) zhinst-timing-models==25.10.0 # via zhinst-utils -zhinst-toolkit==1.1.0 +zhinst-toolkit==1.2.0 # via zhinst-qcodes zhinst-utils==0.7.0 # via zhinst-toolkit From 9d9f8231cbc5990c6661a2b8bfece703c1bc7eca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 04:02:09 +0000 Subject: [PATCH 30/53] Bump libcst from 1.8.5 to 1.8.6 Bumps [libcst](https://github.com/Instagram/LibCST) from 1.8.5 to 1.8.6. - [Release notes](https://github.com/Instagram/LibCST/releases) - [Changelog](https://github.com/Instagram/LibCST/blob/main/CHANGELOG.md) - [Commits](https://github.com/Instagram/LibCST/compare/v1.8.5...v1.8.6) --- updated-dependencies: - dependency-name: libcst dependency-version: 1.8.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fc11f78edf1..30245c89b15 100644 --- a/requirements.txt +++ b/requirements.txt @@ -166,7 +166,7 @@ kiwisolver==1.4.9 # via matplotlib lazy-loader==0.4 # via qcodes-loop -libcst==1.8.5 +libcst==1.8.6 # via qcodes (pyproject.toml) locket==1.0.0 # via partd From 46aff10e19748e7fc70deabf1b59558b2ace685d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 04:02:13 +0000 Subject: [PATCH 31/53] Bump cloudpickle from 3.1.1 to 3.1.2 Bumps [cloudpickle](https://github.com/cloudpipe/cloudpickle) from 3.1.1 to 3.1.2. - [Release notes](https://github.com/cloudpipe/cloudpickle/releases) - [Changelog](https://github.com/cloudpipe/cloudpickle/blob/master/CHANGES.md) - [Commits](https://github.com/cloudpipe/cloudpickle/compare/v3.1.1...v3.1.2) --- updated-dependencies: - dependency-name: cloudpickle dependency-version: 3.1.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fc11f78edf1..b179633c765 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,7 +39,7 @@ click==8.3.0 # via # dask # towncrier -cloudpickle==3.1.1 +cloudpickle==3.1.2 # via dask colorama==0.4.6 # via From 8e1dd133e577c976bd320d095b2e2f614aaaf080 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 04:02:38 +0000 Subject: [PATCH 32/53] Bump hypothesis from 6.144.0 to 6.145.1 Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.144.0 to 6.145.1. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.144.0...hypothesis-python-6.145.1) --- updated-dependencies: - dependency-name: hypothesis dependency-version: 6.145.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fc11f78edf1..16d4c247720 100644 --- a/requirements.txt +++ b/requirements.txt @@ -102,7 +102,7 @@ h5py==3.15.1 # qcodes-loop hickle==5.0.3 # via qcodes-loop -hypothesis==6.144.0 +hypothesis==6.145.1 # via qcodes (pyproject.toml) idna==3.11 # via requests From 18f9a0b0e8f7cc7a5a87ccc6ea8620dcab3de50a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 04:03:20 +0000 Subject: [PATCH 33/53] Bump types-networkx from 3.5.0.20251001 to 3.5.0.20251104 Bumps [types-networkx](https://github.com/typeshed-internal/stub_uploader) from 3.5.0.20251001 to 3.5.0.20251104. - [Commits](https://github.com/typeshed-internal/stub_uploader/commits) --- updated-dependencies: - dependency-name: types-networkx dependency-version: 3.5.0.20251104 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fc11f78edf1..5de78bb984a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -453,7 +453,7 @@ traitlets==5.14.3 # nbsphinx types-jsonschema==4.25.1.20251009 # via qcodes (pyproject.toml) -types-networkx==3.5.0.20251001 +types-networkx==3.5.0.20251104 # via qcodes (pyproject.toml) types-pytz==2025.2.0.20250809 # via pandas-stubs From 7e757160bc02c41469bdd39197d67992532cf02d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 4 Nov 2025 06:59:33 +0100 Subject: [PATCH 34/53] tRemove no longer required assert and add type comments --- .../dataset/descriptions/dependencies.py | 28 ++++++------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/src/qcodes/dataset/descriptions/dependencies.py b/src/qcodes/dataset/descriptions/dependencies.py index 49a98b26b7e..aa3ae017dcf 100644 --- a/src/qcodes/dataset/descriptions/dependencies.py +++ b/src/qcodes/dataset/descriptions/dependencies.py @@ -21,8 +21,6 @@ if TYPE_CHECKING: from collections.abc import Sequence - from networkx.classes.reportviews import DegreeView - from .versioning.rundescribertypes import InterDependencies_Dict _LOGGER = logging.getLogger(__name__) ParamSpecTree = dict[ParamSpecBase, tuple[ParamSpecBase, ...]] @@ -137,9 +135,6 @@ def _validate_acyclic(self, interdeps: ParamSpecTree) -> None: def _validate_no_chained_dependencies(self, interdeps: ParamSpecTree) -> None: for node, in_degree in self._dependency_subgraph.in_degree: out_degree = self._dependency_subgraph.out_degree(node) - assert isinstance(out_degree, int), ( - "The out_degree method with arguments should have returned an int" - ) if in_degree > 0 and out_degree > 0: depends_on_nodes = list(self._dependency_subgraph.successors(node)) depended_on_nodes = list(self._dependency_subgraph.predecessors(node)) @@ -155,6 +150,8 @@ def _dependency_subgraph(self) -> nx.DiGraph[str]: for edge in self.graph.edges if self.graph.edges[edge]["interdep_type"] == "depends_on" ] + # the type annotations does not currently encode that edge_subgraph of a DiGraph + # is a DiGraph return cast("nx.DiGraph[str]", self.graph.edge_subgraph(depends_on_edges)) @property @@ -164,6 +161,8 @@ def _inference_subgraph(self) -> nx.DiGraph[str]: for edge in self.graph.edges if self.graph.edges[edge]["interdep_type"] == "inferred_from" ] + # the type annotations does not currently encode that edge_subgraph of a DiGraph + # is a DiGraph return cast("nx.DiGraph[str]", self.graph.edge_subgraph(inferred_from_edges)) def extend( @@ -195,7 +194,7 @@ def _paramspec_tree_by_type(self, interdep_type: _InterDepType) -> ParamSpecTree return {key: tuple(val) for key, val in paramspec_tree_list.items()} def _node_to_paramspec(self, node_id: str) -> ParamSpecBase: - return cast("ParamSpecBase", self.graph.nodes[node_id]["value"]) + return self.graph.nodes[node_id]["value"] def _paramspec_predecessors_by_type( self, paramspec: ParamSpecBase, interdep_type: _InterDepType @@ -247,13 +246,10 @@ def inferences(self) -> ParamSpecTree: @property def standalones(self) -> frozenset[ParamSpecBase]: - # since we are not requesting the degree of a specific node, we will get a DegreeView - # the type stubs does not yet reflect this so we cast away the int type here - degree_iterator = cast("DegreeView[str]", self.graph.degree) return frozenset( [ self._node_to_paramspec(node_id) - for node_id, degree in degree_iterator + for node_id, degree in self.graph.degree if degree == 0 ] ) @@ -270,10 +266,7 @@ def paramspecs(self) -> tuple[ParamSpecBase, ...]: """ Return the ParamSpecBase objects of this instance """ - return tuple( - cast("ParamSpecBase", paramspec) - for _, paramspec in self.graph.nodes(data="value") - ) + return tuple(paramspec for _, paramspec in self.graph.nodes(data="value")) @property @deprecated( @@ -319,9 +312,7 @@ def top_level_parameters(self) -> tuple[ParamSpecBase, ...]: } standalone_top_level = { self._node_to_paramspec(node_id) - # since we are not requesting the degree of a specific node, we will get a DegreeView - # the type stubs does not yet reflect this so we cast away the int type here - for node_id, degree in cast("DegreeView[str]", self._graph.degree) + for node_id, degree in self._graph.degree if degree == 0 } @@ -349,9 +340,6 @@ def remove(self, paramspec: ParamSpecBase) -> InterDependencies_: to this instance, but has the given parameter removed. """ paramspec_in_degree = self.graph.in_degree(paramspec.name) - assert isinstance(paramspec_in_degree, int), ( - "The in_degree method with arguments should have returned an int" - ) if paramspec_in_degree > 0: raise ValueError( f"Cannot remove {paramspec.name}, other parameters depend on or are inferred from it" From d27441e5bde20d47ff84c83abad63093f2ecf28c Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Tue, 4 Nov 2025 10:01:59 -0800 Subject: [PATCH 35/53] 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 f51f3a987cc..70f7ac98ae8 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 337f63062c0..2b2ed2e0181 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 36/53] 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 9c7755fc842..07a59516173 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 dd77fbf2430..fbeca651a23 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 5968d68027b..c3ac3202801 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 d3a046565ba..3305230337d 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 37/53] 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 d2762b43be2..a148a9657ca 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 38/53] 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 70f7ac98ae8..d676bd15efb 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 d9985d963ec..e7923c1010b 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 2b2ed2e0181..adef5b5e6bd 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 03a75008c77..1ff25916e08 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 c2dff6ef319f80405b3ecf1b0686adbc370f92b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Nov 2025 04:01:43 +0000 Subject: [PATCH 39/53] Bump JamesIves/github-pages-deploy-action from 4.7.3 to 4.7.4 Bumps [JamesIves/github-pages-deploy-action](https://github.com/jamesives/github-pages-deploy-action) from 4.7.3 to 4.7.4. - [Release notes](https://github.com/jamesives/github-pages-deploy-action/releases) - [Commits](https://github.com/jamesives/github-pages-deploy-action/compare/6c2d9db40f9296374acc17b90404b6e8864128c8...4a3abc783e1a24aeb44c16e869ad83caf6b4cc23) --- updated-dependencies: - dependency-name: JamesIves/github-pages-deploy-action dependency-version: 4.7.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/docs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index f829612a2a5..c09fe61d84e 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -126,7 +126,7 @@ jobs: path: build_docs - name: Deploy to gh pages - uses: JamesIves/github-pages-deploy-action@6c2d9db40f9296374acc17b90404b6e8864128c8 # v4.7.3 + uses: JamesIves/github-pages-deploy-action@4a3abc783e1a24aeb44c16e869ad83caf6b4cc23 # v4.7.4 with: branch: gh-pages folder: ${{ github.workspace }}/build_docs/ From 95ec70bc7d350655e8428fbf0446982ccf8f2994 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Nov 2025 04:01:56 +0000 Subject: [PATCH 40/53] Bump step-security/harden-runner from 2.13.1 to 2.13.2 Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.13.1 to 2.13.2. - [Release notes](https://github.com/step-security/harden-runner/releases) - [Commits](https://github.com/step-security/harden-runner/compare/f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a...95d9a5deda9de15063e7595e9719c11c38c90ae2) --- updated-dependencies: - dependency-name: step-security/harden-runner dependency-version: 2.13.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/docs.yaml | 4 ++-- .github/workflows/pre-commit.yml | 2 +- .github/workflows/pytest.yaml | 2 +- .github/workflows/scorecards.yml | 2 +- .github/workflows/upload_to_pypi.yaml | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d2ba849b849..f48aa74b2de 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -30,7 +30,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 with: egress-policy: audit diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index a9e592d8da8..806f6eff68a 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 with: egress-policy: audit diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 03d08b62d1a..7cbadc19b4c 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 with: egress-policy: audit diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index f829612a2a5..85d8e5f4f27 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -43,7 +43,7 @@ jobs: SPHINX_OPTS: "-v -j 2" steps: - name: Harden Runner - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 with: egress-policy: audit - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -113,7 +113,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 with: egress-policy: audit diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 72df7edd2ab..8e851084f98 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 with: egress-policy: audit diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index b29bb99717d..4e7b823d23f 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -52,7 +52,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 with: egress-policy: audit diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 23d8dbe13dd..d1acc09bdb0 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 with: egress-policy: audit diff --git a/.github/workflows/upload_to_pypi.yaml b/.github/workflows/upload_to_pypi.yaml index 1e76b69845d..8148e71f7f9 100644 --- a/.github/workflows/upload_to_pypi.yaml +++ b/.github/workflows/upload_to_pypi.yaml @@ -17,7 +17,7 @@ jobs: id-token: write steps: - name: Harden Runner - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 with: egress-policy: audit From 1c42204dafa0016a4d5f5b1fe9a5c16b7d1d9497 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Nov 2025 04:02:43 +0000 Subject: [PATCH 41/53] Bump types-networkx from 3.5.0.20251104 to 3.5.0.20251106 Bumps [types-networkx](https://github.com/typeshed-internal/stub_uploader) from 3.5.0.20251104 to 3.5.0.20251106. - [Commits](https://github.com/typeshed-internal/stub_uploader/commits) --- updated-dependencies: - dependency-name: types-networkx dependency-version: 3.5.0.20251106 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2322726b99c..82ac431de78 100644 --- a/requirements.txt +++ b/requirements.txt @@ -453,7 +453,7 @@ traitlets==5.14.3 # nbsphinx types-jsonschema==4.25.1.20251009 # via qcodes (pyproject.toml) -types-networkx==3.5.0.20251104 +types-networkx==3.5.0.20251106 # via qcodes (pyproject.toml) types-pytz==2025.2.0.20250809 # via pandas-stubs From ee3d96f960ea326822c8f5f66e186d2f4a57e9fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Nov 2025 04:02:49 +0000 Subject: [PATCH 42/53] Bump ipython from 9.6.0 to 9.7.0 Bumps [ipython](https://github.com/ipython/ipython) from 9.6.0 to 9.7.0. - [Release notes](https://github.com/ipython/ipython/releases) - [Commits](https://github.com/ipython/ipython/compare/9.6.0...9.7.0) --- updated-dependencies: - dependency-name: ipython dependency-version: 9.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2322726b99c..39484d3800b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -116,7 +116,7 @@ ipykernel==7.1.0 # via # qcodes (pyproject.toml) # qcodes -ipython==9.6.0 +ipython==9.7.0 # via # ipykernel # ipywidgets From d28ba255f2bcbac695091d0e6bad102c900b5b19 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Nov 2025 04:03:02 +0000 Subject: [PATCH 43/53] Bump hypothesis from 6.145.1 to 6.146.0 Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.145.1 to 6.146.0. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.145.1...hypothesis-python-6.146.0) --- updated-dependencies: - dependency-name: hypothesis dependency-version: 6.146.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2322726b99c..75f3aab7c3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -102,7 +102,7 @@ h5py==3.15.1 # qcodes-loop hickle==5.0.3 # via qcodes-loop -hypothesis==6.145.1 +hypothesis==6.146.0 # via qcodes (pyproject.toml) idna==3.11 # via requests From 953c5a60d7107133d08658428dd51cf6e255f879 Mon Sep 17 00:00:00 2001 From: Thomas Lemon Date: Thu, 6 Nov 2025 07:46:19 -0800 Subject: [PATCH 44/53] 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 1ff25916e08..caff629fe19 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 45/53] 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 caff629fe19..4ebe96f40f4 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", From 01282c336e4a9ed2fcb7cddc617fbc429dc66c88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Nov 2025 04:02:03 +0000 Subject: [PATCH 46/53] Bump dask from 2025.10.0 to 2025.11.0 Bumps [dask](https://github.com/dask/dask) from 2025.10.0 to 2025.11.0. - [Release notes](https://github.com/dask/dask/releases) - [Changelog](https://github.com/dask/dask/blob/main/docs/release-procedure.md) - [Commits](https://github.com/dask/dask/compare/2025.10.0...2025.11.0) --- updated-dependencies: - dependency-name: dask dependency-version: 2025.11.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index dc68dee3afb..4f7721ca4f0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,7 +60,7 @@ coverage==7.11.0 # pytest-cov cycler==0.12.1 # via matplotlib -dask==2025.10.0 +dask==2025.11.0 # via # qcodes (pyproject.toml) # qcodes From b6940ce5ed857eb20b0889a00eb9b3ec770514e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Nov 2025 04:02:17 +0000 Subject: [PATCH 47/53] Bump hypothesis from 6.146.0 to 6.147.0 Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.146.0 to 6.147.0. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.146.0...hypothesis-python-6.147.0) --- updated-dependencies: - dependency-name: hypothesis dependency-version: 6.147.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index dc68dee3afb..c240cd17afb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -102,7 +102,7 @@ h5py==3.15.1 # qcodes-loop hickle==5.0.3 # via qcodes-loop -hypothesis==6.146.0 +hypothesis==6.147.0 # via qcodes (pyproject.toml) idna==3.11 # via requests From d81927472933dc83f4771f4c5562cd4c889140b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 04:02:18 +0000 Subject: [PATCH 48/53] Bump types-pytz from 2025.2.0.20250809 to 2025.2.0.20251108 Bumps [types-pytz](https://github.com/typeshed-internal/stub_uploader) from 2025.2.0.20250809 to 2025.2.0.20251108. - [Commits](https://github.com/typeshed-internal/stub_uploader/commits) --- updated-dependencies: - dependency-name: types-pytz dependency-version: 2025.2.0.20251108 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f6616157f1a..336bd436ffb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -455,7 +455,7 @@ types-jsonschema==4.25.1.20251009 # via qcodes (pyproject.toml) types-networkx==3.5.0.20251106 # via qcodes (pyproject.toml) -types-pytz==2025.2.0.20250809 +types-pytz==2025.2.0.20251108 # via pandas-stubs types-pywin32==311.0.0.20251008 # via qcodes (pyproject.toml) From 3cdfaf559bba83825f0fbc3325bbe599f6637ec4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 04:02:48 +0000 Subject: [PATCH 49/53] Bump coverage from 7.11.0 to 7.11.3 Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.11.0 to 7.11.3. - [Release notes](https://github.com/coveragepy/coveragepy/releases) - [Changelog](https://github.com/coveragepy/coveragepy/blob/main/CHANGES.rst) - [Commits](https://github.com/coveragepy/coveragepy/compare/7.11.0...7.11.3) --- updated-dependencies: - dependency-name: coverage dependency-version: 7.11.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f6616157f1a..d9e991e42df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -54,7 +54,7 @@ comm==0.2.3 # ipywidgets contourpy==1.3.3 # via matplotlib -coverage==7.11.0 +coverage==7.11.3 # via # qcodes (pyproject.toml) # pytest-cov From ea85d15cbeaf7714c9dbacc28523b1175cade5b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 19:56:22 +0000 Subject: [PATCH 50/53] Bump pytest-asyncio from 1.2.0 to 1.3.0 Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 1.2.0 to 1.3.0. - [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) - [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v1.2.0...v1.3.0) --- updated-dependencies: - dependency-name: pytest-asyncio dependency-version: 1.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 075fa1d42c9..bb7c040f9ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -310,7 +310,7 @@ pytest==8.4.2 # pytest-mock # pytest-rerunfailures # pytest-xdist -pytest-asyncio==1.2.0 +pytest-asyncio==1.3.0 # via qcodes (pyproject.toml) pytest-cov==7.0.0 # via qcodes (pyproject.toml) From 8b3ba18c1fb91fdd0c22535ae28c697804b63188 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 04:05:51 +0000 Subject: [PATCH 51/53] Bump actions/dependency-review-action from 4.8.1 to 4.8.2 Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4.8.1 to 4.8.2. - [Release notes](https://github.com/actions/dependency-review-action/releases) - [Commits](https://github.com/actions/dependency-review-action/compare/40c09b7dc99638e5ddb0bfd91c1673effc064d8a...3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261) --- updated-dependencies: - dependency-name: actions/dependency-review-action dependency-version: 4.8.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/dependency-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 7cbadc19b4c..f9e9713724f 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -24,4 +24,4 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: 'Dependency Review' - uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1 + uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2 From 4863c392aee5ba33e1bbe3db0447b46c59d6a55a Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Fri, 12 Dec 2025 15:09:53 +0000 Subject: [PATCH 52/53] Merge pull request #7712 from jenshnielsen/improve_speed Remove graph related overhead from data storage --- docs/changes/newsfragments/7712.improved | 2 + src/qcodes/dataset/data_set.py | 7 +- src/qcodes/dataset/data_set_in_memory.py | 7 +- .../dataset/descriptions/dependencies.py | 184 +++++++++++++++++- src/qcodes/dataset/measurements.py | 25 ++- .../test_measurement_context_manager.py | 14 ++ tests/dataset/test_dependencies.py | 58 ++++++ 7 files changed, 283 insertions(+), 14 deletions(-) create mode 100644 docs/changes/newsfragments/7712.improved diff --git a/docs/changes/newsfragments/7712.improved b/docs/changes/newsfragments/7712.improved new file mode 100644 index 00000000000..ea8ab0f2382 --- /dev/null +++ b/docs/changes/newsfragments/7712.improved @@ -0,0 +1,2 @@ +The `InterDependencies_` class is now frozen during the performance of a measurement so it cannot be modified. +This enables caching of attributes on the class significantly reducing the overhead of measurements. diff --git a/src/qcodes/dataset/data_set.py b/src/qcodes/dataset/data_set.py index 55e8d221b55..57d65da24b1 100644 --- a/src/qcodes/dataset/data_set.py +++ b/src/qcodes/dataset/data_set.py @@ -566,7 +566,10 @@ def toggle_debug(self) -> None: self.conn = connect(path_to_db, self._debug) def set_interdependencies( - self, interdeps: InterDependencies_, shapes: Shapes | None = None + self, + interdeps: InterDependencies_, + shapes: Shapes | None = None, + override: bool = False, ) -> None: """ Set the interdependencies object (which holds all added @@ -579,7 +582,7 @@ def set_interdependencies( f"Wrong input type. Expected InterDepencies_, got {type(interdeps)}" ) - if not self.pristine: + if not self.pristine and not override: mssg = "Can not set interdependencies on a DataSet that has been started." raise RuntimeError(mssg) self._rundescriber = RunDescriber(interdeps, shapes=shapes) diff --git a/src/qcodes/dataset/data_set_in_memory.py b/src/qcodes/dataset/data_set_in_memory.py index bbd8d0bd2ae..f64c0f40a0c 100644 --- a/src/qcodes/dataset/data_set_in_memory.py +++ b/src/qcodes/dataset/data_set_in_memory.py @@ -748,7 +748,10 @@ def _set_parent_dataset_links(self, links: list[Link]) -> None: self._parent_dataset_links = links def _set_interdependencies( - self, interdeps: InterDependencies_, shapes: Shapes | None = None + self, + interdeps: InterDependencies_, + shapes: Shapes | None = None, + override: bool = False, ) -> None: """ Set the interdependencies object (which holds all added @@ -761,7 +764,7 @@ def _set_interdependencies( f"Wrong input type. Expected InterDepencies_, got {type(interdeps)}" ) - if not self.pristine: + if not self.pristine and not override: mssg = "Can not set interdependencies on a DataSet that has been started." raise RuntimeError(mssg) self._rundescriber = RunDescriber(interdeps, shapes=shapes) diff --git a/src/qcodes/dataset/descriptions/dependencies.py b/src/qcodes/dataset/descriptions/dependencies.py index aa3ae017dcf..5d23129520f 100644 --- a/src/qcodes/dataset/descriptions/dependencies.py +++ b/src/qcodes/dataset/descriptions/dependencies.py @@ -428,6 +428,18 @@ def validate_paramspectree( else: raise ValueError(f"Invalid {interdep_type_internal}") from TypeError(cause) + def _invalid_subsets( + self, paramspecs: Sequence[ParamSpecBase] + ) -> tuple[set[str], set[str]] | None: + subset_nodes = {paramspec.name for paramspec in paramspecs} + for subset_node in subset_nodes: + descendant_nodes_per_subset_node = nx.descendants(self.graph, subset_node) + if missing_nodes := descendant_nodes_per_subset_node.difference( + subset_nodes + ): + return (subset_nodes, missing_nodes) + return None + def validate_subset(self, paramspecs: Sequence[ParamSpecBase]) -> None: """ Validate that the given parameters form a valid subset of the @@ -442,15 +454,11 @@ def validate_subset(self, paramspecs: Sequence[ParamSpecBase]) -> None: InterdependencyError: If a dependency or inference is missing """ - subset_nodes = set([paramspec.name for paramspec in paramspecs]) - for subset_node in subset_nodes: - descendant_nodes_per_subset_node = nx.descendants(self.graph, subset_node) - if missing_nodes := descendant_nodes_per_subset_node.difference( - subset_nodes - ): - raise IncompleteSubsetError( - subset_params=subset_nodes, missing_params=missing_nodes - ) + invalid_subset = self._invalid_subsets(paramspecs) + if invalid_subset is not None: + raise IncompleteSubsetError( + subset_params=invalid_subset[0], missing_params=invalid_subset[1] + ) @classmethod def _from_graph(cls, graph: nx.DiGraph[str]) -> InterDependencies_: @@ -624,3 +632,161 @@ def paramspec_tree_to_param_name_tree( return { key.name: [item.name for item in items] for key, items in paramspec_tree.items() } + + +class FrozenInterDependencies_(InterDependencies_): # noqa: PLW1641 + # todo: not clear if this should implement __hash__. + """ + A frozen version of InterDependencies_ that is immutable and caches + expensive lookups. This is used exclusively while running a measurement + to minimize the overhead of dependency lookups for each data operation. + + Args: + interdeps: An InterDependencies_ instance to freeze + + """ + + def __init__(self, interdeps: InterDependencies_): + self._graph = interdeps.graph.copy() + nx.freeze(self._graph) + self._top_level_parameters_cache: tuple[ParamSpecBase, ...] | None = None + self._dependencies_cache: ParamSpecTree | None = None + self._inferences_cache: ParamSpecTree | None = None + self._standalones_cache: frozenset[ParamSpecBase] | None = None + self._find_all_parameters_in_tree_cache: dict[ + ParamSpecBase, set[ParamSpecBase] + ] = {} + self._invalid_subsets_cache: dict[ + tuple[ParamSpecBase, ...], tuple[set[str], set[str]] | None + ] = {} + self._id_to_paramspec_cache: dict[str, ParamSpecBase] | None = None + self._paramspec_to_id_cache: dict[ParamSpecBase, str] | None = None + + def add_dependencies(self, dependencies: ParamSpecTree | None) -> None: + raise TypeError("FrozenInterDependencies_ is immutable") + + def add_inferences(self, inferences: ParamSpecTree | None) -> None: + raise TypeError("FrozenInterDependencies_ is immutable") + + def add_standalones(self, standalones: tuple[ParamSpecBase, ...]) -> None: + raise TypeError("FrozenInterDependencies_ is immutable") + + def add_paramspecs(self, paramspecs: Sequence[ParamSpecBase]) -> None: + raise TypeError("FrozenInterDependencies_ is immutable") + + def remove(self, paramspec: ParamSpecBase) -> InterDependencies_: + raise TypeError("FrozenInterDependencies_ is immutable") + + def extend( + self, + dependencies: ParamSpecTree | None = None, + inferences: ParamSpecTree | None = None, + standalones: tuple[ParamSpecBase, ...] = (), + ) -> InterDependencies_: + """ + Create a new :class:`InterDependencies_` object + that is an extension of this instance with the provided input + """ + # We need to unfreeze the graph for the new instance + new_graph = nx.DiGraph(self.graph) + new_interdependencies = InterDependencies_._from_graph(new_graph) + + new_interdependencies.add_dependencies(dependencies) + new_interdependencies.add_inferences(inferences) + new_interdependencies.add_standalones(standalones) + return new_interdependencies + + @property + def top_level_parameters(self) -> tuple[ParamSpecBase, ...]: + if self._top_level_parameters_cache is None: + self._top_level_parameters_cache = super().top_level_parameters + return self._top_level_parameters_cache + + @property + def dependencies(self) -> ParamSpecTree: + if self._dependencies_cache is None: + self._dependencies_cache = super().dependencies + return self._dependencies_cache.copy() + + @property + def inferences(self) -> ParamSpecTree: + if self._inferences_cache is None: + self._inferences_cache = super().inferences + return self._inferences_cache.copy() + + @property + def standalones(self) -> frozenset[ParamSpecBase]: + if self._standalones_cache is None: + self._standalones_cache = super().standalones + return self._standalones_cache + + def find_all_parameters_in_tree( + self, initial_param: ParamSpecBase + ) -> set[ParamSpecBase]: + if initial_param not in self._find_all_parameters_in_tree_cache: + self._find_all_parameters_in_tree_cache[initial_param] = ( + super().find_all_parameters_in_tree(initial_param) + ) + return self._find_all_parameters_in_tree_cache[initial_param].copy() + + @classmethod + def _from_dict(cls, ser: InterDependencies_Dict) -> FrozenInterDependencies_: + interdeps = InterDependencies_._from_dict(ser) + return cls(interdeps) + + @classmethod + def _from_graph(cls, graph: nx.DiGraph[str]) -> FrozenInterDependencies_: + interdeps = InterDependencies_._from_graph(graph) + return cls(interdeps) + + def validate_subset(self, paramspecs: Sequence[ParamSpecBase]) -> None: + paramspecs_tuple = tuple(paramspecs) + if paramspecs_tuple not in self._invalid_subsets_cache: + self._invalid_subsets_cache[paramspecs_tuple] = self._invalid_subsets( + paramspecs_tuple + ) + invalid_subset = self._invalid_subsets_cache[paramspecs_tuple] + if invalid_subset is not None: + raise IncompleteSubsetError( + subset_params=invalid_subset[0], missing_params=invalid_subset[1] + ) + + @property + def _id_to_paramspec(self) -> dict[str, ParamSpecBase]: + if self._id_to_paramspec_cache is None: + self._id_to_paramspec_cache = { + node_id: data["value"] for node_id, data in self.graph.nodes(data=True) + } + return self._id_to_paramspec_cache + + @property + def _paramspec_to_id(self) -> dict[ParamSpecBase, str]: + if self._paramspec_to_id_cache is None: + self._paramspec_to_id_cache = { + data["value"]: node_id for node_id, data in self.graph.nodes(data=True) + } + return self._paramspec_to_id_cache + + def __repr__(self) -> str: + rep = ( + f"FrozenInterDependencies_(dependencies={self.dependencies}, " + f"inferences={self.inferences}, " + f"standalones={self.standalones})" + ) + return rep + + def __eq__(self, other: object) -> bool: + if not isinstance(other, FrozenInterDependencies_): + return False + return nx.utils.graphs_equal(self.graph, other.graph) + + def to_interdependencies(self) -> InterDependencies_: + """ + Convert this FrozenInterDependencies_ back to a mutable InterDependencies_ instance. + + Returns: + A new InterDependencies_ instance with the same data as this frozen instance. + + """ + new_graph = nx.DiGraph(self.graph) + return InterDependencies_._from_graph(new_graph) diff --git a/src/qcodes/dataset/measurements.py b/src/qcodes/dataset/measurements.py index 6eb4c054df3..dbf015cb055 100644 --- a/src/qcodes/dataset/measurements.py +++ b/src/qcodes/dataset/measurements.py @@ -36,6 +36,7 @@ ValuesType, ) from qcodes.dataset.descriptions.dependencies import ( + FrozenInterDependencies_, IncompleteSubsetError, InterDependencies_, ParamSpecTree, @@ -759,6 +760,28 @@ def __exit__( self._span.record_exception(exception_value) self.ds.add_metadata("measurement_exception", exception_string) + # for now we set the interdependencies back to the + # not frozen state, so that further modifications are possible + # this is not recommended but we want to minimize the changes for now + + if isinstance(self.ds.description.interdeps, FrozenInterDependencies_): + intedeps = self.ds.description.interdeps.to_interdependencies() + else: + intedeps = self.ds.description.interdeps + + if isinstance(self.ds, DataSet): + self.ds.set_interdependencies( + shapes=self.ds.description.shapes, + interdeps=intedeps, + override=True, + ) + elif isinstance(self.ds, DataSetInMem): + self.ds._set_interdependencies( + shapes=self.ds.description.shapes, + interdeps=intedeps, + override=True, + ) + # and finally mark the dataset as closed, thus # finishing the measurement # Note that the completion of a dataset entails waiting for the @@ -1508,7 +1531,7 @@ def run( self.experiment, station=self.station, write_period=self._write_period, - interdeps=self._interdeps, + interdeps=FrozenInterDependencies_(self._interdeps), name=self.name, subscribers=self.subscribers, parent_datasets=self._parent_datasets, diff --git a/tests/dataset/measurement/test_measurement_context_manager.py b/tests/dataset/measurement/test_measurement_context_manager.py index 550a59e91c3..4377b81148a 100644 --- a/tests/dataset/measurement/test_measurement_context_manager.py +++ b/tests/dataset/measurement/test_measurement_context_manager.py @@ -21,6 +21,10 @@ import qcodes as qc import qcodes.validators as vals from qcodes.dataset.data_set import DataSet, load_by_id +from qcodes.dataset.descriptions.dependencies import ( + FrozenInterDependencies_, + InterDependencies_, +) from qcodes.dataset.experiment_container import new_experiment from qcodes.dataset.export_config import DataExportType from qcodes.dataset.measurements import Measurement @@ -730,6 +734,16 @@ def test_datasaver_scalars( with pytest.raises(ValueError): datasaver.add_result((DMM.v1, 0)) + ds = datasaver.dataset + assert isinstance(ds, DataSet) + assert isinstance(ds.description.interdeps, InterDependencies_) + assert not isinstance(ds.description.interdeps, FrozenInterDependencies_) + + loaded_ds = load_by_id(ds.run_id) + + assert isinstance(loaded_ds.description.interdeps, InterDependencies_) + assert not isinstance(loaded_ds.description.interdeps, FrozenInterDependencies_) + # More assertions of setpoints, labels and units in the DB! diff --git a/tests/dataset/test_dependencies.py b/tests/dataset/test_dependencies.py index e2607655ad5..c4deebc3223 100644 --- a/tests/dataset/test_dependencies.py +++ b/tests/dataset/test_dependencies.py @@ -6,6 +6,7 @@ from networkx import NetworkXError from qcodes.dataset.descriptions.dependencies import ( + FrozenInterDependencies_, IncompleteSubsetError, InterDependencies_, ) @@ -477,3 +478,60 @@ def test_dependency_on_middle_parameter( # in both directions, ps4 is actually a member of the tree for ps1 assert idps.top_level_parameters == (ps1,) assert idps.find_all_parameters_in_tree(ps1) == {ps1, ps2, ps3, ps4} + + +def test_frozen_interdependencies(some_paramspecbases) -> None: + ps1, ps2, ps3, ps4 = some_paramspecbases + idps = InterDependencies_(dependencies={ps1: (ps2, ps3)}, inferences={ps2: (ps4,)}) + + frozen = FrozenInterDependencies_(idps) + + assert frozen.dependencies == idps.dependencies + assert frozen.inferences == idps.inferences + assert frozen.standalones == idps.standalones + assert frozen.top_level_parameters == idps.top_level_parameters + + # Test immutability + with pytest.raises(TypeError, match="FrozenInterDependencies_ is immutable"): + frozen.add_dependencies({ps4: (ps1,)}) + + with pytest.raises(TypeError, match="FrozenInterDependencies_ is immutable"): + frozen.add_inferences({ps4: (ps1,)}) + + with pytest.raises(TypeError, match="FrozenInterDependencies_ is immutable"): + frozen.add_standalones((ps4,)) + + with pytest.raises(TypeError, match="FrozenInterDependencies_ is immutable"): + frozen.remove(ps1) + + with pytest.raises(TypeError, match="FrozenInterDependencies_ is immutable"): + frozen.add_paramspecs((ps1,)) + + # Test extend returns InterDependencies_ (mutable) + ps5 = ParamSpecBase("psb5", "numeric", "number", "") + extended = frozen.extend(standalones=(ps5,)) + assert isinstance(extended, InterDependencies_) + assert not isinstance(extended, FrozenInterDependencies_) + assert ps5 in extended.standalones + + # Test caching of properties + # Access properties to trigger caching + _ = frozen.dependencies + _ = frozen.inferences + _ = frozen.standalones + _ = frozen.top_level_parameters + + assert frozen._dependencies_cache is not None + assert frozen._inferences_cache is not None + assert frozen._standalones_cache is not None + assert frozen._top_level_parameters_cache is not None + + +def test_frozen_from_dict(some_paramspecbases) -> None: + ps1, ps2, ps3, _ = some_paramspecbases + idps = InterDependencies_(dependencies={ps1: (ps2, ps3)}) + ser = idps._to_dict() + + frozen = FrozenInterDependencies_._from_dict(ser) + assert isinstance(frozen, FrozenInterDependencies_) + assert frozen == FrozenInterDependencies_(idps) From 573eb8a39f7b1c94439ed83439c58795daf9d2d8 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 12 Dec 2025 16:58:03 +0100 Subject: [PATCH 53/53] Add changelog for 0.54.3 and 0.54.4 --- docs/changes/0.54.3.rst | 18 ++++++++++++++++++ docs/changes/0.54.4.rst | 8 ++++++++ docs/changes/index.rst | 2 ++ .../changes/newsfragments/7542.improved_driver | 3 --- docs/changes/newsfragments/7606.improved | 3 --- docs/changes/newsfragments/7607.improved | 2 -- docs/changes/newsfragments/7712.improved | 2 -- 7 files changed, 28 insertions(+), 10 deletions(-) create mode 100644 docs/changes/0.54.3.rst create mode 100644 docs/changes/0.54.4.rst delete mode 100644 docs/changes/newsfragments/7542.improved_driver delete mode 100644 docs/changes/newsfragments/7606.improved delete mode 100644 docs/changes/newsfragments/7607.improved delete mode 100644 docs/changes/newsfragments/7712.improved diff --git a/docs/changes/0.54.3.rst b/docs/changes/0.54.3.rst new file mode 100644 index 00000000000..1b13b39d086 --- /dev/null +++ b/docs/changes/0.54.3.rst @@ -0,0 +1,18 @@ +QCoDeS 0.54.3 (2025-11-11) +========================== + +Improved: +--------- + +- - 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. (:pr:`7606`) +- Fixes a bug in the LinSweeper iterator that caused it to always raise StopIteration after + completing a single sweep. This bug meant LinSweeper could not be used in a nested measurement function. (:pr:`7607`) + +Improved Drivers: +----------------- + +- The Stanford SR86x drivers now statically assign attributes statically + for more member InstrumentModules and parameters enabling better documentation, + type checking and IDE integration. (:pr:`7542`) diff --git a/docs/changes/0.54.4.rst b/docs/changes/0.54.4.rst new file mode 100644 index 00000000000..1fbe0c8856b --- /dev/null +++ b/docs/changes/0.54.4.rst @@ -0,0 +1,8 @@ +QCoDeS 0.54.4 (2025-12-12) +========================== + +Improved: +--------- + +- The `InterDependencies_` class is now frozen during the performance of a measurement so it cannot be modified. + This enables caching of attributes on the class significantly reducing the overhead of measurements. (:pr:`7712`) diff --git a/docs/changes/index.rst b/docs/changes/index.rst index 1dc364c68f7..42d07dd40d4 100644 --- a/docs/changes/index.rst +++ b/docs/changes/index.rst @@ -3,6 +3,8 @@ Changelogs .. toctree:: Unreleased + 0.54.4 <0.54.4> + 0.54.3 <0.54.3> 0.54.1 <0.54.1> 0.54.0 <0.54.0> 0.53.0 <0.53.0> diff --git a/docs/changes/newsfragments/7542.improved_driver b/docs/changes/newsfragments/7542.improved_driver deleted file mode 100644 index 60c9f25745d..00000000000 --- a/docs/changes/newsfragments/7542.improved_driver +++ /dev/null @@ -1,3 +0,0 @@ -The Stanford SR86x drivers now statically assign attributes statically -for more member InstrumentModules and parameters enabling better documentation, -type checking and IDE integration. diff --git a/docs/changes/newsfragments/7606.improved b/docs/changes/newsfragments/7606.improved deleted file mode 100644 index da0fac5bd17..00000000000 --- a/docs/changes/newsfragments/7606.improved +++ /dev/null @@ -1,3 +0,0 @@ -- 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. diff --git a/docs/changes/newsfragments/7607.improved b/docs/changes/newsfragments/7607.improved deleted file mode 100644 index b6ad1438466..00000000000 --- a/docs/changes/newsfragments/7607.improved +++ /dev/null @@ -1,2 +0,0 @@ -Fixes a bug in the LinSweeper iterator that caused it to always raise StopIteration after -completing a single sweep. This bug meant LinSweeper could not be used in a nested measurement function. diff --git a/docs/changes/newsfragments/7712.improved b/docs/changes/newsfragments/7712.improved deleted file mode 100644 index ea8ab0f2382..00000000000 --- a/docs/changes/newsfragments/7712.improved +++ /dev/null @@ -1,2 +0,0 @@ -The `InterDependencies_` class is now frozen during the performance of a measurement so it cannot be modified. -This enables caching of attributes on the class significantly reducing the overhead of measurements.