Skip to content

Commit ab6131a

Browse files
authored
Merge pull request #7398 from trlemon/feature/add-cryomagnetics-tm620-driver
Add Driver for Cryomagnetics TM-620 Cryogenic Temperature Monitor
2 parents 6b2bb64 + 3cf224d commit ab6131a

File tree

6 files changed

+349
-0
lines changed

6 files changed

+349
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added Cryomagnetics TM-620 driver
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Cryomagnetics TM-620 QCoDeS Driver Example\n",
8+
"\n",
9+
"This notebook showcases how to interface with a Cryomagnetics TM-620 Cryogenic Temperature Monitor using QCoDeS. The Cryomagnetics TM-620 Cryogenic Temperature Monitor is commonly used in research settings for precise temperature monitoring, including superconductivity studies and material science.\n",
10+
"\n",
11+
"## Setup\n",
12+
"\n",
13+
"First, ensure you have the required drivers and QCoDeS installed. The connection to the instrument is assumed to be via GPIB. Let's establish the connection and set up the initial parameters for reading temperatures.\n",
14+
"\n",
15+
"Please note that you will need to update the address according to your connection type (e.g., USB, Ethernet, etc.)"
16+
]
17+
},
18+
{
19+
"cell_type": "code",
20+
"execution_count": null,
21+
"metadata": {},
22+
"outputs": [],
23+
"source": [
24+
"from qcodes.instrument_drivers.cryomagnetics import CryomagneticsModelTM620\n",
25+
"\n",
26+
"tm620 = CryomagneticsModelTM620(\n",
27+
" name=\"cryomag_tm620\",\n",
28+
" address=\"GPIB::1::INSTR\",\n",
29+
" pyvisa_sim_file=\"cryo_tm620.yaml\",\n",
30+
")"
31+
]
32+
},
33+
{
34+
"cell_type": "markdown",
35+
"metadata": {},
36+
"source": [
37+
"## Basic Operations\n",
38+
"\n",
39+
"The following sections demonstrate how to perform basic operations with the Cryomagnetics TM-620 instrument. We will cover checking the temperatures of both the shield and magnet parameters."
40+
]
41+
},
42+
{
43+
"cell_type": "code",
44+
"execution_count": null,
45+
"metadata": {},
46+
"outputs": [],
47+
"source": [
48+
"# Reading the shield temperature\n",
49+
"shield_temp = tm620.shield()\n",
50+
"print(f\"55K Shield Temperature = {shield_temp}K\")\n",
51+
"\n",
52+
"# Reading the magnet temperature\n",
53+
"magnet_temp = tm620.magnet()\n",
54+
"print(f\"4K Magnet Temperature = {magnet_temp}K\")"
55+
]
56+
},
57+
{
58+
"cell_type": "code",
59+
"execution_count": null,
60+
"metadata": {},
61+
"outputs": [],
62+
"source": [
63+
"import time\n",
64+
"\n",
65+
"import matplotlib.pyplot as plt\n",
66+
"\n",
67+
"\n",
68+
"# Function to plot shield and magnet temperatures over time.\n",
69+
"def plot_temperature_over_time(cryo_instr, duration, measurement_interval=0.5):\n",
70+
" times = []\n",
71+
" shield_temps = []\n",
72+
" magnet_temps = []\n",
73+
" start_time = time.time()\n",
74+
" while (time.time() - start_time) < duration:\n",
75+
" shield_temps.append(cryo_instr.shield())\n",
76+
" magnet_temps.append(cryo_instr.magnet())\n",
77+
" times.append(time.time() - start_time)\n",
78+
" time.sleep(measurement_interval)\n",
79+
" plt.plot(times, shield_temps, marker=\"o\", label=\"55K Shield Temp\")\n",
80+
" plt.plot(times, magnet_temps, marker=\"x\", label=\"4K Magnet Temp\")\n",
81+
" plt.xlabel(\"Time (s)\")\n",
82+
" plt.ylabel(\"Temperature (K)\")\n",
83+
" plt.title(\"Time vs Temperature (K)\")\n",
84+
" plt.legend()\n",
85+
" plt.show()\n",
86+
"\n",
87+
"\n",
88+
"# Example usage:\n",
89+
"plot_temperature_over_time(\n",
90+
" tm620, 60\n",
91+
") # plot for 60 seconds while setting the field to 1 T"
92+
]
93+
},
94+
{
95+
"cell_type": "markdown",
96+
"metadata": {},
97+
"source": [
98+
"### Notes\n",
99+
"\n",
100+
"For further information, consult the official documentation and user manuals provided by the manufacturer. Always prioritize safe operating practices and maintain the instrument according to the recommended guidelines."
101+
]
102+
}
103+
],
104+
"metadata": {
105+
"kernelspec": {
106+
"display_name": "qcodes",
107+
"language": "python",
108+
"name": "python3"
109+
},
110+
"language_info": {
111+
"codemirror_mode": {
112+
"name": "ipython",
113+
"version": 3
114+
},
115+
"file_extension": ".py",
116+
"mimetype": "text/x-python",
117+
"name": "python",
118+
"nbconvert_exporter": "python",
119+
"pygments_lexer": "ipython3",
120+
"version": "3.13.5"
121+
}
122+
},
123+
"nbformat": 4,
124+
"nbformat_minor": 4
125+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
spec: "1.0"
2+
devices:
3+
cryomag_tm620:
4+
eom:
5+
GPIB INSTR:
6+
q: "\r\n"
7+
r: "\r\n"
8+
error: ERROR
9+
dialogues:
10+
- q: "*IDN?"
11+
r: "Cryomagnetics,TM-620_simulation,2002,2.00"
12+
- q: "MEAS? A"
13+
r: "{RANDOM(54, 56, 1):.2f}K\r\n"
14+
- q: "MEAS? B"
15+
r: "{RANDOM(3, 5, 1):.2f}K\r\n"
16+
17+
properties:
18+
shield:
19+
getter:
20+
q: "MEAS? A"
21+
r: "{RANDOM(54, 56, 1):.2f}K\r\n"
22+
magnet:
23+
getter:
24+
q: "MEAS? B"
25+
r: "{RANDOM(3, 5, 1):.2f}K\r\n"
26+
27+
28+
resources:
29+
GPIB::1::INSTR:
30+
device: cryomag_tm620
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import re
2+
from typing import TYPE_CHECKING
3+
4+
from qcodes.instrument import VisaInstrument, VisaInstrumentKWArgs
5+
6+
if TYPE_CHECKING:
7+
from typing_extensions import Unpack
8+
9+
from qcodes.parameters import Parameter
10+
11+
12+
class CryomagneticsModelTM620(VisaInstrument):
13+
"""
14+
Driver for the Cryomagnetics TM 620 temperature monitor.
15+
16+
Units are kG right now
17+
18+
Args:
19+
name: a name for the instrument
20+
21+
address: VISA address of the device
22+
23+
"""
24+
25+
float_pattern = re.compile(r"[0-9]+\.[0-9]+")
26+
27+
def __init__(
28+
self,
29+
name: str,
30+
address: str,
31+
**kwargs: "Unpack[VisaInstrumentKWArgs]",
32+
) -> None:
33+
super().__init__(name, address, **kwargs)
34+
35+
self.shield: Parameter = self.add_parameter(
36+
name="shield",
37+
unit="K",
38+
get_cmd=self._get_A,
39+
get_parser=float,
40+
docstring="55K Shield Temp",
41+
)
42+
"""55K shield temperature"""
43+
44+
self.magnet: Parameter = self.add_parameter(
45+
name="magnet",
46+
unit="K",
47+
get_cmd=self._get_B,
48+
get_parser=float,
49+
docstring="4K Magnet Temp",
50+
)
51+
"""4K magnet temperature"""
52+
53+
self.operating_mode()
54+
self.connect_message()
55+
56+
def operating_mode(self, remote: bool = True) -> None:
57+
"""
58+
Sets the device's operating mode to either remote or local.
59+
60+
Args:
61+
remote: If True, sets to remote mode, otherwise sets to local mode.
62+
63+
"""
64+
if remote:
65+
self.write("REMOTE")
66+
else:
67+
self.write("LOCAL")
68+
69+
def _get_A(self) -> float:
70+
"""Get 55k shield temperature
71+
72+
Returns:
73+
Temperature in Kelvin
74+
75+
"""
76+
output = self.ask("MEAS? A")
77+
output = self._parse_output(output)
78+
numeric_output = self._convert_to_numeric(output)
79+
80+
return numeric_output
81+
82+
def _get_B(self) -> float:
83+
"""Get 4k magnet temp
84+
85+
Returns:
86+
Temperature in Kelvin
87+
88+
"""
89+
output = self.ask("MEAS? B")
90+
output = self._parse_output(output)
91+
numeric_output = self._convert_to_numeric(output)
92+
93+
return numeric_output
94+
95+
def _parse_output(self, output: str) -> str:
96+
"""Extract floating point number from the instrument output string.
97+
98+
Args:
99+
output: the string returned from the instrument.
100+
101+
Returns:
102+
parsed string containing extracted floating point number.
103+
104+
"""
105+
106+
match = self.float_pattern.search(output)
107+
108+
if match:
109+
return match.group(0)
110+
111+
self.log.error(f"No floating point number found in output: '{output}'")
112+
raise ValueError(f"No floating point number found in output: '{output}'")
113+
114+
def _convert_to_numeric(self, raw_value: str) -> float:
115+
"""
116+
Convert a raw string value to a numeric float.
117+
118+
Args:
119+
raw_value: The raw string value to convert.
120+
121+
Returns:
122+
The converted float value.
123+
124+
"""
125+
try:
126+
numeric_value = float(raw_value)
127+
return numeric_value
128+
except ValueError:
129+
self.log.error(f"Error converting '{raw_value}' to float")
130+
raise ValueError(f"Unable to convert '{raw_value}' to float")

src/qcodes/instrument_drivers/cryomagnetics/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
CryomagneticsModel4G,
55
CryomagneticsOperatingState,
66
)
7+
from ._TM620 import CryomagneticsModelTM620
78

89
__all__ = [
910
"Cryomagnetics4GException",
1011
"Cryomagnetics4GWarning",
1112
"CryomagneticsModel4G",
13+
"CryomagneticsModelTM620",
1214
"CryomagneticsOperatingState",
1315
]
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from unittest.mock import patch
2+
3+
import pytest
4+
5+
from qcodes.instrument_drivers.cryomagnetics import CryomagneticsModelTM620
6+
7+
8+
@pytest.fixture(name="tm620", scope="function")
9+
def fixture_tm620():
10+
"""
11+
Fixture to create and yield a CryomagneticsModelTM620 object and close it after testing.
12+
"""
13+
instrument = CryomagneticsModelTM620(
14+
name="test_cryo_tm620",
15+
address="GPIB::2::INSTR",
16+
terminator="\r\n",
17+
pyvisa_sim_file="cryo_tm620.yaml",
18+
)
19+
yield instrument
20+
instrument.close()
21+
22+
23+
def test_initialization(tm620):
24+
assert tm620.name == "test_cryo_tm620"
25+
assert tm620._address == "GPIB::2::INSTR"
26+
assert hasattr(tm620, "shield")
27+
assert hasattr(tm620, "magnet")
28+
29+
30+
def test_get_A_success(tm620):
31+
with patch.object(tm620, "ask", return_value="55.12K"):
32+
assert tm620._get_A() == 55.12
33+
34+
35+
def test_get_B_success(tm620):
36+
with patch.object(tm620, "ask", return_value="4.21K"):
37+
assert tm620._get_B() == 4.21
38+
39+
40+
def test_parse_output_valid(tm620):
41+
assert tm620._parse_output("55.12K") == "55.12"
42+
assert tm620._parse_output("4.34C") == "4.34"
43+
44+
45+
def test_parse_output_invalid(tm620, caplog):
46+
with caplog.at_level("ERROR"):
47+
with pytest.raises(ValueError, match="No floating point number found"):
48+
tm620._parse_output("Invalid output")
49+
assert "No floating point number found in output" in caplog.text
50+
51+
52+
def test_convert_to_numerics_valid(tm620):
53+
assert tm620._convert_to_numeric("55.12") == 55.12
54+
assert tm620._convert_to_numeric("4.34") == 4.34
55+
56+
57+
def test_convert_to_numerics_invalid(tm620, caplog):
58+
with caplog.at_level("ERROR"):
59+
with pytest.raises(ValueError, match="Unable to convert"):
60+
tm620._convert_to_numeric("not_a_number")
61+
assert "Error converting 'not_a_number' to float" in caplog.text

0 commit comments

Comments
 (0)