pychemstation 0.4.7.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ag_hplc_macro/__init__.py +3 -0
- ag_hplc_macro/analysis/__init__.py +1 -0
- ag_hplc_macro/analysis/base_spectrum.py +509 -0
- ag_hplc_macro/analysis/spec_utils.py +304 -0
- ag_hplc_macro/analysis/utils.py +63 -0
- ag_hplc_macro/control/__init__.py +5 -0
- ag_hplc_macro/control/chromatogram.py +128 -0
- ag_hplc_macro/control/hplc.py +673 -0
- ag_hplc_macro/generated/__init__.py +56 -0
- ag_hplc_macro/generated/dad_method.py +367 -0
- ag_hplc_macro/generated/pump_method.py +519 -0
- ag_hplc_macro/utils/__init__.py +2 -0
- ag_hplc_macro/utils/chemstation.py +290 -0
- ag_hplc_macro/utils/constants.py +15 -0
- ag_hplc_macro/utils/hplc_param_types.py +185 -0
- hein-analytical-control/__init__.py +3 -0
- hein-analytical-control/analysis/__init__.py +1 -0
- hein-analytical-control/analysis/base_spectrum.py +509 -0
- hein-analytical-control/analysis/spec_utils.py +304 -0
- hein-analytical-control/analysis/utils.py +63 -0
- hein-analytical-control/devices/Agilent/__init__.py +3 -0
- hein-analytical-control/devices/Agilent/chemstation.py +290 -0
- hein-analytical-control/devices/Agilent/chromatogram.py +129 -0
- hein-analytical-control/devices/Agilent/hplc.py +436 -0
- hein-analytical-control/devices/Agilent/hplc_param_types.py +141 -0
- hein-analytical-control/devices/Magritek/Spinsolve/__init__.py +0 -0
- hein-analytical-control/devices/Magritek/Spinsolve/commands.py +495 -0
- hein-analytical-control/devices/Magritek/Spinsolve/spectrum.py +822 -0
- hein-analytical-control/devices/Magritek/Spinsolve/spinsolve.py +425 -0
- hein-analytical-control/devices/Magritek/Spinsolve/utils/__init__.py +5 -0
- hein-analytical-control/devices/Magritek/Spinsolve/utils/connection.py +168 -0
- hein-analytical-control/devices/Magritek/Spinsolve/utils/constants.py +8 -0
- hein-analytical-control/devices/Magritek/Spinsolve/utils/exceptions.py +25 -0
- hein-analytical-control/devices/Magritek/Spinsolve/utils/parser.py +340 -0
- hein-analytical-control/devices/Magritek/Spinsolve/utils/shimming.py +55 -0
- hein-analytical-control/devices/Magritek/Spinsolve/utils/spinsolve_logging.py +43 -0
- hein-analytical-control/devices/Magritek/__init__.py +0 -0
- hein-analytical-control/devices/OceanOptics/IR/NIRQuest512.py +90 -0
- hein-analytical-control/devices/OceanOptics/IR/__init__.py +0 -0
- hein-analytical-control/devices/OceanOptics/IR/ir_spectrum.py +191 -0
- hein-analytical-control/devices/OceanOptics/Raman/__init__.py +0 -0
- hein-analytical-control/devices/OceanOptics/Raman/raman_control.py +46 -0
- hein-analytical-control/devices/OceanOptics/Raman/raman_spectrum.py +148 -0
- hein-analytical-control/devices/OceanOptics/UV/QEPro2192.py +90 -0
- hein-analytical-control/devices/OceanOptics/UV/__init__.py +0 -0
- hein-analytical-control/devices/OceanOptics/UV/uv_spectrum.py +227 -0
- hein-analytical-control/devices/OceanOptics/__init__.py +0 -0
- hein-analytical-control/devices/OceanOptics/oceanoptics.py +115 -0
- hein-analytical-control/devices/__init__.py +15 -0
- hein-analytical-control/generated/__init__.py +56 -0
- hein-analytical-control/generated/dad_method.py +367 -0
- hein-analytical-control/generated/pump_method.py +519 -0
- hein_analytical_control/__init__.py +3 -0
- hein_analytical_control/analysis/__init__.py +1 -0
- hein_analytical_control/analysis/base_spectrum.py +509 -0
- hein_analytical_control/analysis/spec_utils.py +304 -0
- hein_analytical_control/analysis/utils.py +63 -0
- hein_analytical_control/devices/Agilent/__init__.py +3 -0
- hein_analytical_control/devices/Agilent/chemstation.py +290 -0
- hein_analytical_control/devices/Agilent/chromatogram.py +129 -0
- hein_analytical_control/devices/Agilent/hplc.py +436 -0
- hein_analytical_control/devices/Agilent/hplc_param_types.py +141 -0
- hein_analytical_control/devices/Magritek/Spinsolve/__init__.py +0 -0
- hein_analytical_control/devices/Magritek/Spinsolve/commands.py +495 -0
- hein_analytical_control/devices/Magritek/Spinsolve/spectrum.py +822 -0
- hein_analytical_control/devices/Magritek/Spinsolve/spinsolve.py +425 -0
- hein_analytical_control/devices/Magritek/Spinsolve/utils/__init__.py +5 -0
- hein_analytical_control/devices/Magritek/Spinsolve/utils/connection.py +168 -0
- hein_analytical_control/devices/Magritek/Spinsolve/utils/constants.py +8 -0
- hein_analytical_control/devices/Magritek/Spinsolve/utils/exceptions.py +25 -0
- hein_analytical_control/devices/Magritek/Spinsolve/utils/parser.py +340 -0
- hein_analytical_control/devices/Magritek/Spinsolve/utils/shimming.py +55 -0
- hein_analytical_control/devices/Magritek/Spinsolve/utils/spinsolve_logging.py +43 -0
- hein_analytical_control/devices/Magritek/__init__.py +0 -0
- hein_analytical_control/devices/OceanOptics/IR/NIRQuest512.py +90 -0
- hein_analytical_control/devices/OceanOptics/IR/__init__.py +0 -0
- hein_analytical_control/devices/OceanOptics/IR/ir_spectrum.py +191 -0
- hein_analytical_control/devices/OceanOptics/Raman/__init__.py +0 -0
- hein_analytical_control/devices/OceanOptics/Raman/raman_control.py +46 -0
- hein_analytical_control/devices/OceanOptics/Raman/raman_spectrum.py +148 -0
- hein_analytical_control/devices/OceanOptics/UV/QEPro2192.py +90 -0
- hein_analytical_control/devices/OceanOptics/UV/__init__.py +0 -0
- hein_analytical_control/devices/OceanOptics/UV/uv_spectrum.py +227 -0
- hein_analytical_control/devices/OceanOptics/__init__.py +0 -0
- hein_analytical_control/devices/OceanOptics/oceanoptics.py +115 -0
- hein_analytical_control/devices/__init__.py +15 -0
- hein_analytical_control/generated/__init__.py +56 -0
- hein_analytical_control/generated/dad_method.py +367 -0
- hein_analytical_control/generated/pump_method.py +519 -0
- pychemstation/__init__.py +3 -0
- pychemstation/analysis/__init__.py +1 -0
- pychemstation/analysis/base_spectrum.py +509 -0
- pychemstation/analysis/spec_utils.py +304 -0
- pychemstation/analysis/utils.py +63 -0
- pychemstation/control/__init__.py +5 -0
- pychemstation/control/chromatogram.py +128 -0
- pychemstation/control/hplc.py +673 -0
- pychemstation/generated/__init__.py +56 -0
- pychemstation/generated/dad_method.py +367 -0
- pychemstation/generated/pump_method.py +519 -0
- pychemstation/utils/__init__.py +2 -0
- pychemstation/utils/chemstation.py +290 -0
- pychemstation/utils/constants.py +15 -0
- pychemstation/utils/hplc_param_types.py +185 -0
- pychemstation-0.4.7.dev1.dist-info/LICENSE +395 -0
- pychemstation-0.4.7.dev1.dist-info/METADATA +102 -0
- pychemstation-0.4.7.dev1.dist-info/RECORD +109 -0
- pychemstation-0.4.7.dev1.dist-info/WHEEL +5 -0
- pychemstation-0.4.7.dev1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,425 @@
|
|
1
|
+
"""
|
2
|
+
Module provide API for the remote control of the Magritek SpinSolve NMR
|
3
|
+
"""
|
4
|
+
import queue
|
5
|
+
import threading
|
6
|
+
import warnings
|
7
|
+
import os
|
8
|
+
import json
|
9
|
+
import time
|
10
|
+
|
11
|
+
from .utils import ReplyParser, SpinsolveConnection, get_logger
|
12
|
+
from .utils.exceptions import HardwareError
|
13
|
+
from .utils.shimming import shimming
|
14
|
+
from .commands import ProtocolCommands, RequestCommands
|
15
|
+
from .spectrum import SpinsolveNMRSpectrum
|
16
|
+
|
17
|
+
|
18
|
+
# shimming parameters are stored here
|
19
|
+
# after shimming operation has been performed
|
20
|
+
SHIMMING_PATH = os.path.join(os.path.dirname(__file__), "utils", "shimming.json")
|
21
|
+
|
22
|
+
|
23
|
+
class SpinsolveNMR:
|
24
|
+
"""Python class to handle Magritek Spinsolve NMR instrument"""
|
25
|
+
|
26
|
+
DEFAULT_EXPERIMENT = ("1D PROTON", {"Scan": "StandardScan"})
|
27
|
+
|
28
|
+
def __init__(
|
29
|
+
self, spinsolve_options_path=None, address=None, port=13000, auto_connect=True
|
30
|
+
):
|
31
|
+
"""
|
32
|
+
Args:
|
33
|
+
spinsolve_options_path (str, optional): Valid path to the ProtocolOptions.xml
|
34
|
+
address (str, optional): IP address of the local host
|
35
|
+
host (int, optional): host for the TCP/IP connection to the Spinsolve software
|
36
|
+
auto_connect (bool, optional): If you need to connect to the instrument immediately
|
37
|
+
after instantiation
|
38
|
+
"""
|
39
|
+
|
40
|
+
self.logger = get_logger()
|
41
|
+
|
42
|
+
# Queue for storing path of the measured spectrum
|
43
|
+
self.data_folder_queue = queue.Queue()
|
44
|
+
|
45
|
+
# Flag for check the instrument status
|
46
|
+
self._device_ready_flag = threading.Event()
|
47
|
+
|
48
|
+
# Instantiating submodules
|
49
|
+
self._parser = ReplyParser(self._device_ready_flag, self.data_folder_queue)
|
50
|
+
self._connection = SpinsolveConnection(HOST=address, PORT=port)
|
51
|
+
self.cmd = ProtocolCommands(spinsolve_options_path)
|
52
|
+
self.req_cmd = RequestCommands()
|
53
|
+
self.spectrum = SpinsolveNMRSpectrum()
|
54
|
+
|
55
|
+
if auto_connect:
|
56
|
+
self.connect()
|
57
|
+
self.initialise()
|
58
|
+
|
59
|
+
# placeholder to store shimming parameters
|
60
|
+
self.last_shimming_results = {}
|
61
|
+
|
62
|
+
# placeholders for experiment data
|
63
|
+
self._user_data = {}
|
64
|
+
self._solvent = None
|
65
|
+
self._sample = None
|
66
|
+
|
67
|
+
def check_last_shimming(self):
|
68
|
+
"""Checks last shimming.
|
69
|
+
|
70
|
+
Returns:
|
71
|
+
bool: False if shimming procedure is required, True otherwise.
|
72
|
+
"""
|
73
|
+
if not self.last_shimming_results:
|
74
|
+
try:
|
75
|
+
with open(SHIMMING_PATH) as fobj:
|
76
|
+
self.last_shimming_results = json.load(fobj)
|
77
|
+
except FileNotFoundError:
|
78
|
+
self.logger.warning(
|
79
|
+
"Last shimming was not recorded, please run\
|
80
|
+
any shimming protocol to update!"
|
81
|
+
)
|
82
|
+
return False
|
83
|
+
now = time.time()
|
84
|
+
# if the last shimming was performed more than 24 hours ago
|
85
|
+
if now - self.last_shimming_results["timestamp"] > 24 * 3600:
|
86
|
+
self.logger.critical(
|
87
|
+
"Last shimming was performed more than 24 \
|
88
|
+
hours ago, please perform CheckShim to check spectrometer performance!"
|
89
|
+
)
|
90
|
+
return False
|
91
|
+
return True
|
92
|
+
|
93
|
+
def connect(self):
|
94
|
+
"""Connects to the instrument"""
|
95
|
+
|
96
|
+
self.logger.debug("Connection requested")
|
97
|
+
self._connection.open_connection()
|
98
|
+
|
99
|
+
def disconnect(self):
|
100
|
+
"""Closes the socket connection"""
|
101
|
+
|
102
|
+
self.logger.info("Request to close the connection received")
|
103
|
+
self._connection.close_connection()
|
104
|
+
self.logger.info("The instrument is disconnected")
|
105
|
+
|
106
|
+
def send_message(self, msg):
|
107
|
+
"""Sends the message to the instrument"""
|
108
|
+
|
109
|
+
if self._parser.connected_tag != "true":
|
110
|
+
raise HardwareError(
|
111
|
+
"The instrument is not connected, check the Spinsolve software"
|
112
|
+
)
|
113
|
+
self.logger.debug("Waiting for the device to be ready")
|
114
|
+
self._device_ready_flag.wait()
|
115
|
+
self.logger.debug("Sending the message \n%s", msg)
|
116
|
+
self._connection.transmit(msg)
|
117
|
+
self.logger.debug("Message sent")
|
118
|
+
|
119
|
+
def receive_reply(self, parse=True):
|
120
|
+
"""Receives the reply from the instrument and parses it if necessary"""
|
121
|
+
|
122
|
+
while True:
|
123
|
+
self.logger.debug("Reply requested from the connection")
|
124
|
+
reply = self._connection.receive()
|
125
|
+
self.logger.debug("Reply received")
|
126
|
+
if parse:
|
127
|
+
reply = self._parser.parse(reply)
|
128
|
+
if self._device_ready_flag.is_set():
|
129
|
+
return reply
|
130
|
+
|
131
|
+
def initialise(self):
|
132
|
+
"""Initialises the instrument by sending HardwareRequest"""
|
133
|
+
|
134
|
+
cmd = self.req_cmd.request_hardware()
|
135
|
+
self._connection.transmit(cmd)
|
136
|
+
return self.receive_reply()
|
137
|
+
|
138
|
+
def is_instrument_ready(self):
|
139
|
+
"""Checks if the instrument is ready for the next command"""
|
140
|
+
|
141
|
+
if self._parser.connected_tag == "true" and self._device_ready_flag.is_set():
|
142
|
+
return True
|
143
|
+
else:
|
144
|
+
return False
|
145
|
+
|
146
|
+
def load_commands(self):
|
147
|
+
"""Requests the available commands from the instrument"""
|
148
|
+
|
149
|
+
cmd = self.req_cmd.request_available_protocol_options()
|
150
|
+
self.send_message(cmd)
|
151
|
+
reply = self.receive_reply()
|
152
|
+
self.cmd.reload_commands(reply)
|
153
|
+
self.logger.info(
|
154
|
+
"Commands updated, see available protocols \n <%s>",
|
155
|
+
list(self.cmd._protocols.keys()),
|
156
|
+
) # pylint: disable=protected-access
|
157
|
+
|
158
|
+
@shimming
|
159
|
+
def shim(
|
160
|
+
self,
|
161
|
+
option="CheckShimRequest",
|
162
|
+
*,
|
163
|
+
line_width_threshold=1,
|
164
|
+
base_width_threshold=40,
|
165
|
+
):
|
166
|
+
"""Initialise shimming protocol
|
167
|
+
|
168
|
+
Consider checking <Spinsolve>.cmd.get_protocol(<Spinsolve>.cmd.SHIM_PROTOCOL) for available options
|
169
|
+
|
170
|
+
Args:
|
171
|
+
option (str, optional): A name of the instrument shimming method
|
172
|
+
"""
|
173
|
+
|
174
|
+
# updating default values
|
175
|
+
self._parser.shimming_line_width_threshold = line_width_threshold
|
176
|
+
self._parser.shimming_base_width_threshold = base_width_threshold
|
177
|
+
|
178
|
+
cmd = self.req_cmd.request_shim(option)
|
179
|
+
self.send_message(cmd)
|
180
|
+
return self.receive_reply()
|
181
|
+
|
182
|
+
@shimming
|
183
|
+
def shim_on_sample(
|
184
|
+
self,
|
185
|
+
reference_peak,
|
186
|
+
option="LockAndCalibrateOnly",
|
187
|
+
*,
|
188
|
+
line_width_threshold=1,
|
189
|
+
base_width_threshold=40,
|
190
|
+
):
|
191
|
+
"""Initialise shimming on sample protocol
|
192
|
+
|
193
|
+
Consider checking <Spinsolve>.cmd.get_protocol(<Spinsolve>.cmd.SHIM_ON_SAMPLE_PROTOCOL) for available options
|
194
|
+
|
195
|
+
Args:
|
196
|
+
reference_peak (float): A reference peak to shim and calibrate on
|
197
|
+
option (str, optional): A name of the instrument shimming method
|
198
|
+
line_width_threshold (float, optional): Spectrum line width at 50%, should be below 1
|
199
|
+
for good quality spectrums
|
200
|
+
base_width_threshold (float, optional): Spectrum line width at 0.55%, should be below 40
|
201
|
+
for good quality spectrums
|
202
|
+
"""
|
203
|
+
|
204
|
+
self._parser.shimming_line_width_threshold = line_width_threshold
|
205
|
+
self._parser.shimming_base_width_threshold = base_width_threshold
|
206
|
+
cmd = self.cmd.shim_on_sample(reference_peak, option)
|
207
|
+
self.send_message(cmd)
|
208
|
+
return self.receive_reply()
|
209
|
+
|
210
|
+
def set_user_folder(self, data_path, data_folder_method="TimeStamp"):
|
211
|
+
"""Indicate the path and the method for saving NMR data
|
212
|
+
|
213
|
+
Args:
|
214
|
+
data_folder_path (str): Valid path to save the spectral data
|
215
|
+
data_folder_method (str, optional): One of three methods according to the manual:
|
216
|
+
'UserFolder' - Data is saved directly in the provided path
|
217
|
+
'TimeStamp' (default) - Data is saved in newly created folder in format
|
218
|
+
yyyymmddhhmmss in the provided path
|
219
|
+
'TimeStampTree' - Data is saved in the newly created folders in format
|
220
|
+
yyyy/mm/dd/hh/mm/ss in the provided path
|
221
|
+
|
222
|
+
Returns:
|
223
|
+
bool: True if successfull
|
224
|
+
"""
|
225
|
+
|
226
|
+
cmd = self.req_cmd.set_data_folder(data_path, data_folder_method)
|
227
|
+
self.send_message(cmd)
|
228
|
+
return True
|
229
|
+
|
230
|
+
@property
|
231
|
+
def user_data(self):
|
232
|
+
"""Dictionary with user specific data."""
|
233
|
+
if not self._user_data:
|
234
|
+
user_data_req = self.req_cmd.get_user_data()
|
235
|
+
self.send_message(user_data_req)
|
236
|
+
self._user_data = self.receive_reply()
|
237
|
+
|
238
|
+
return self._user_data
|
239
|
+
|
240
|
+
@user_data.setter
|
241
|
+
def user_data(self, user_data):
|
242
|
+
"""Sets the user data.
|
243
|
+
|
244
|
+
Args:
|
245
|
+
user_data (Dict): Dictionary with user data.
|
246
|
+
"""
|
247
|
+
# updating placeholder
|
248
|
+
self._user_data.update(user_data)
|
249
|
+
# sending command
|
250
|
+
user_data_cmd = self.req_cmd.set_user_data(user_data)
|
251
|
+
self.send_message(user_data_cmd)
|
252
|
+
|
253
|
+
@user_data.deleter
|
254
|
+
def user_data(self):
|
255
|
+
"""Removes previously stored user data."""
|
256
|
+
|
257
|
+
# generating command to reset the data in spinsolve
|
258
|
+
empty_user_data_command = self.req_cmd.set_user_data(
|
259
|
+
{key: "" for key in self._user_data}
|
260
|
+
)
|
261
|
+
self.send_message(empty_user_data_command)
|
262
|
+
|
263
|
+
# updating placeholder
|
264
|
+
self._user_data = {}
|
265
|
+
|
266
|
+
@property
|
267
|
+
def solvent(self):
|
268
|
+
"""Solvent record to be stored with spectrum acquisition params."""
|
269
|
+
if self._solvent is None:
|
270
|
+
solvent_req = self.req_cmd.get_solvent()
|
271
|
+
self.send_message(solvent_req)
|
272
|
+
self._solvent = self.receive_reply()
|
273
|
+
return self._solvent
|
274
|
+
|
275
|
+
@solvent.setter
|
276
|
+
def solvent(self, solvent):
|
277
|
+
"""Sets the solvent record for the current experiment."""
|
278
|
+
self._solvent = solvent
|
279
|
+
solvent_data_cmd = self.req_cmd.set_solvent_data(solvent)
|
280
|
+
self.send_message(solvent_data_cmd)
|
281
|
+
|
282
|
+
@solvent.deleter
|
283
|
+
def solvent(self):
|
284
|
+
"""Removes the solvent record for the current experiment."""
|
285
|
+
self._solvent = None
|
286
|
+
empty_solvent_data_cmd = self.req_cmd.set_solvent_data("")
|
287
|
+
self.send_message(empty_solvent_data_cmd)
|
288
|
+
|
289
|
+
@property
|
290
|
+
def sample(self):
|
291
|
+
"""Sample record to be stored with spectrum acquisition params."""
|
292
|
+
if self._sample is None:
|
293
|
+
sample_req = self.req_cmd.get_sample()
|
294
|
+
self.send_message(sample_req)
|
295
|
+
self._sample = self.receive_reply()
|
296
|
+
return self._sample
|
297
|
+
|
298
|
+
@sample.setter
|
299
|
+
def sample(self, sample):
|
300
|
+
"""Sets the sample record for the current experiment.
|
301
|
+
|
302
|
+
Also sets the folder to save the spectrum, so avoid special characters.
|
303
|
+
"""
|
304
|
+
self._sample = sample
|
305
|
+
sample_data_cmd = self.req_cmd.set_sample_data(sample)
|
306
|
+
self.send_message(sample_data_cmd)
|
307
|
+
|
308
|
+
@sample.deleter
|
309
|
+
def sample(self):
|
310
|
+
"""Removes the sample record for the current experiment."""
|
311
|
+
self._sample = None
|
312
|
+
empty_sample_data_cmd = self.req_cmd.set_sample_data("")
|
313
|
+
self.send_message(empty_sample_data_cmd)
|
314
|
+
|
315
|
+
def get_duration(self, protocol, options):
|
316
|
+
"""Requests for an approximate duration of a specific protocol
|
317
|
+
|
318
|
+
Args:
|
319
|
+
protocol (str): A name of the specific protocol
|
320
|
+
options (dict): Options for the selected protocol
|
321
|
+
"""
|
322
|
+
|
323
|
+
cmd = self.cmd.generate_command(
|
324
|
+
(protocol, options), self.cmd.ESTIMATE_DURATION_REQUEST
|
325
|
+
)
|
326
|
+
self.send_message(cmd)
|
327
|
+
return self.receive_reply()
|
328
|
+
|
329
|
+
def proton(self, option="QuickScan"):
|
330
|
+
"""Initialise simple 1D Proton experiment"""
|
331
|
+
|
332
|
+
cmd = self.cmd.generate_command((self.cmd.PROTON, {"Scan": f"{option}"}))
|
333
|
+
self.send_message(cmd)
|
334
|
+
return self.receive_reply()
|
335
|
+
|
336
|
+
def proton_extended(self, options):
|
337
|
+
"""Initialise extended 1D Proton experiment"""
|
338
|
+
|
339
|
+
cmd = self.cmd.generate_command((self.cmd.PROTON_EXTENDED, options))
|
340
|
+
self.send_message(cmd)
|
341
|
+
return self.receive_reply()
|
342
|
+
|
343
|
+
def carbon(self, options=None):
|
344
|
+
"""Initialise simple 1D Carbon experiment"""
|
345
|
+
|
346
|
+
if options is None:
|
347
|
+
options = {"Number": "128", "RepetitionTime": "2"}
|
348
|
+
cmd = self.cmd.generate_command((self.cmd.CARBON, options))
|
349
|
+
self.send_message(cmd)
|
350
|
+
return self.receive_reply()
|
351
|
+
|
352
|
+
def carbon_extended(self, options):
|
353
|
+
"""Initialise extended 1D Carbon experiment"""
|
354
|
+
|
355
|
+
cmd = self.cmd.generate_command((self.cmd.CARBON_EXTENDED, options))
|
356
|
+
self.send_message(cmd)
|
357
|
+
return self.receive_reply()
|
358
|
+
|
359
|
+
def fluorine(self, option="QuickScan"):
|
360
|
+
"""Initialise simple 1D Fluorine experiment"""
|
361
|
+
|
362
|
+
cmd = self.cmd.generate_command((self.cmd.FLUORINE, option))
|
363
|
+
self.send_message(cmd)
|
364
|
+
return self.receive_reply()
|
365
|
+
|
366
|
+
def fluorine_extended(self, options):
|
367
|
+
"""Initialise extended 1D Fluorine experiment"""
|
368
|
+
|
369
|
+
cmd = self.cmd.generate_command((self.cmd.FLUORINE_EXTENDED, options))
|
370
|
+
self.send_message(cmd)
|
371
|
+
return self.receive_reply()
|
372
|
+
|
373
|
+
def wait_until_ready(self):
|
374
|
+
"""Blocks until the instrument is ready"""
|
375
|
+
|
376
|
+
self._device_ready_flag.wait()
|
377
|
+
|
378
|
+
def calibrate(self, reference_peak, option="LockAndCalibrateOnly"):
|
379
|
+
"""Performs shimming on sample protocol"""
|
380
|
+
|
381
|
+
self.logger.warning("DEPRECATION WARNING: use shim_on_sample() method instead")
|
382
|
+
return self.shim_on_sample(reference_peak, option)
|
383
|
+
|
384
|
+
@property
|
385
|
+
def protocols_list(self):
|
386
|
+
"""Returns a list of all available protocols"""
|
387
|
+
|
388
|
+
return list(self.cmd)
|
389
|
+
|
390
|
+
def get_spectrum(self, protocol=None):
|
391
|
+
"""Wrapper method to load the spectral data to inner Spectrum class.
|
392
|
+
|
393
|
+
Loads the last measured data. If no data previously measured, will
|
394
|
+
perform self.DEFAULT_EXPERIMENT and load its data.
|
395
|
+
"""
|
396
|
+
|
397
|
+
if self.data_folder_queue.empty():
|
398
|
+
self.logger.warning("No previous data.")
|
399
|
+
if protocol is None:
|
400
|
+
protocol = self.DEFAULT_EXPERIMENT
|
401
|
+
self.logger.warning(
|
402
|
+
"Running default <%s> protocol.", self.DEFAULT_EXPERIMENT[0]
|
403
|
+
)
|
404
|
+
cmd = self.cmd.generate_command(protocol)
|
405
|
+
self.send_message(cmd)
|
406
|
+
self.receive_reply()
|
407
|
+
|
408
|
+
# will block if spectrum is measuring
|
409
|
+
data_folder = self.data_folder_queue.get()
|
410
|
+
|
411
|
+
self.spectrum.load_spectrum(data_folder)
|
412
|
+
|
413
|
+
warning_message = 'Method "get_spectrum" will no longer return the \
|
414
|
+
spectropic data. Please use .spectrum class to access the spectral data and \
|
415
|
+
to the documentation for its usage.'
|
416
|
+
|
417
|
+
warnings.warn(warning_message, DeprecationWarning)
|
418
|
+
|
419
|
+
# for backwards compatibility
|
420
|
+
data1d = os.path.join(data_folder, "data.1d")
|
421
|
+
_, fid_real, fid_img = self.spectrum.extract_data(data1d)
|
422
|
+
|
423
|
+
fid_complex = [complex(real, img) for real, img in zip(fid_real, fid_img)]
|
424
|
+
|
425
|
+
return fid_complex
|
@@ -0,0 +1,168 @@
|
|
1
|
+
"""
|
2
|
+
Python module for connection with Spinsolve NMR software.
|
3
|
+
"""
|
4
|
+
import logging
|
5
|
+
import socket
|
6
|
+
import threading
|
7
|
+
import queue
|
8
|
+
|
9
|
+
|
10
|
+
class SpinsolveConnection:
|
11
|
+
"""Provides API for the socket connection to the Spinsolve NMR instrument"""
|
12
|
+
|
13
|
+
def __init__(self, HOST=None, PORT=13000):
|
14
|
+
"""
|
15
|
+
Args:
|
16
|
+
HOST (str, optional): TCP/IP address of the local host
|
17
|
+
PORT (int, optional): TCP/IP listening port for Spinsolve software, 13000 by default
|
18
|
+
must be changed in the software if necessary
|
19
|
+
"""
|
20
|
+
|
21
|
+
# Getting the localhost IP address if not provided by instantiation
|
22
|
+
# refer to socket module manual for details
|
23
|
+
try:
|
24
|
+
CURR_HOST = socket.gethostbyname(socket.getfqdn())
|
25
|
+
except socket.gaierror:
|
26
|
+
CURR_HOST = socket.gethostbyname(socket.gethostname())
|
27
|
+
|
28
|
+
# Connection parameters
|
29
|
+
if HOST is not None:
|
30
|
+
self.HOST = HOST
|
31
|
+
else:
|
32
|
+
self.HOST = CURR_HOST
|
33
|
+
self.PORT = PORT
|
34
|
+
|
35
|
+
# The buffer size is so big for the only large message sent by the instrument - whole list of
|
36
|
+
# Protocol options. One day will be reduced with addition of non-blocking parser/connection
|
37
|
+
# TODO
|
38
|
+
self.BUFSIZE = 65536
|
39
|
+
|
40
|
+
# Connection object, thread, lock and disconnection request tag
|
41
|
+
self._listener = None
|
42
|
+
self._connection = None
|
43
|
+
self._connection_close_requested = threading.Event()
|
44
|
+
|
45
|
+
# Response queue for inter threading commincation
|
46
|
+
self.response_queue = queue.Queue()
|
47
|
+
|
48
|
+
self.logger = logging.getLogger("spinsolve.connection")
|
49
|
+
|
50
|
+
def open_connection(self):
|
51
|
+
"""Open a socket connection to the Spinsolve software"""
|
52
|
+
|
53
|
+
if self._connection is not None:
|
54
|
+
self.logger.warning(
|
55
|
+
"You are trying to open connection that is already open"
|
56
|
+
)
|
57
|
+
return
|
58
|
+
|
59
|
+
# Creating socket
|
60
|
+
self._connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
61
|
+
self._connection.settimeout(None)
|
62
|
+
|
63
|
+
# Connecting and spawning listening thread
|
64
|
+
try:
|
65
|
+
self._connection.connect((self.HOST, self.PORT))
|
66
|
+
except ConnectionRefusedError:
|
67
|
+
self._connection = None # Resetting the internal attribute
|
68
|
+
raise ConnectionRefusedError(
|
69
|
+
"Please run Spinsolve software and enable remote control!"
|
70
|
+
)
|
71
|
+
self.logger.debug("Connection at %s:%s is opened", self.HOST, self.PORT)
|
72
|
+
self._listener = threading.Thread(
|
73
|
+
target=self.connection_listener,
|
74
|
+
name="{}_listener".format(__name__),
|
75
|
+
daemon=False,
|
76
|
+
)
|
77
|
+
self._listener.start()
|
78
|
+
self.logger.info("Connection created")
|
79
|
+
|
80
|
+
def connection_listener(self):
|
81
|
+
"""Checks for the new data and output it into receive buffer"""
|
82
|
+
|
83
|
+
self.logger.info("Connection listener thread is starting")
|
84
|
+
|
85
|
+
while True:
|
86
|
+
try:
|
87
|
+
# Receiving data
|
88
|
+
chunk = self._connection.recv(self.BUFSIZE)
|
89
|
+
self.logger.debug("New chunk %s", chunk.decode())
|
90
|
+
self.response_queue.put(chunk)
|
91
|
+
self.logger.debug("Message added to the response queue")
|
92
|
+
except ConnectionAbortedError:
|
93
|
+
self.logger.warning("Connection aborted")
|
94
|
+
break
|
95
|
+
except ConnectionResetError:
|
96
|
+
self.logger.warning("Spinsolve app is closed")
|
97
|
+
break
|
98
|
+
except OSError:
|
99
|
+
self.logger.warning("Connection error")
|
100
|
+
break
|
101
|
+
self.logger.info("Exiting listening thread")
|
102
|
+
|
103
|
+
def transmit(self, msg):
|
104
|
+
"""Sends the message to the socket
|
105
|
+
|
106
|
+
Args:
|
107
|
+
msg (bytes): encoded message to be sent to the instrument
|
108
|
+
"""
|
109
|
+
|
110
|
+
self.logger.debug("Sending the message")
|
111
|
+
# This is necessary due to a random bug in the Spinsolve software with
|
112
|
+
# wrong order of the messages sent.
|
113
|
+
# See details in AnalyticalLabware/issues/22
|
114
|
+
while True:
|
115
|
+
try:
|
116
|
+
unprocessed = self.response_queue.get_nowait()
|
117
|
+
self.logger.error(
|
118
|
+
"Unprocessed message obtained from the response queue, \
|
119
|
+
see below:\n%s",
|
120
|
+
unprocessed,
|
121
|
+
)
|
122
|
+
except queue.Empty:
|
123
|
+
break
|
124
|
+
self._connection.send(msg)
|
125
|
+
self.logger.debug("Message sent")
|
126
|
+
|
127
|
+
def receive(self):
|
128
|
+
"""Grabs the message from receive buffer"""
|
129
|
+
|
130
|
+
self.logger.debug("Receiving the message from the responce queue")
|
131
|
+
reply = self.response_queue.get()
|
132
|
+
self.response_queue.task_done()
|
133
|
+
self.logger.debug("Message obtained from the queue")
|
134
|
+
|
135
|
+
return reply
|
136
|
+
|
137
|
+
def close_connection(self):
|
138
|
+
"""Closes connection"""
|
139
|
+
|
140
|
+
self.logger.debug("Socket connection closure requested")
|
141
|
+
self._connection_close_requested.set()
|
142
|
+
if self._connection is not None:
|
143
|
+
self._connection.shutdown(socket.SHUT_RDWR)
|
144
|
+
self._connection.close()
|
145
|
+
self._connection = None # To available subsequent calls to open_connection after connection was once closed
|
146
|
+
self._connection_close_requested.clear()
|
147
|
+
self.logger.info("Socket connection closed")
|
148
|
+
else:
|
149
|
+
self.logger.warning("You are trying to close nonexistent connection")
|
150
|
+
if self._listener is not None and self._listener.is_alive():
|
151
|
+
self._listener.join(timeout=3)
|
152
|
+
|
153
|
+
def _flush_the_queue(self):
|
154
|
+
while True:
|
155
|
+
try:
|
156
|
+
data = self.response_queue.get_nowait()
|
157
|
+
if data:
|
158
|
+
self.logger.warning(
|
159
|
+
"Response queue flushed, something inside %s", data
|
160
|
+
)
|
161
|
+
self.response_queue.task_done()
|
162
|
+
except queue.Empty:
|
163
|
+
break
|
164
|
+
|
165
|
+
def is_connection_open(self):
|
166
|
+
"""Checks if the connection to the instrument is still alive"""
|
167
|
+
# TODO
|
168
|
+
raise NotImplementedError
|
@@ -0,0 +1,25 @@
|
|
1
|
+
"""Module containts general SpinSolve errors"""
|
2
|
+
|
3
|
+
|
4
|
+
class HardwareError(Exception):
|
5
|
+
"""Generic error in hardware operation"""
|
6
|
+
|
7
|
+
|
8
|
+
class NMRError(Exception):
|
9
|
+
"""Generic error in NMR operation"""
|
10
|
+
|
11
|
+
|
12
|
+
class ProtocolError(NMRError):
|
13
|
+
"""Generic error in Protocol handling"""
|
14
|
+
|
15
|
+
|
16
|
+
class ProtocolOptionsError(KeyError):
|
17
|
+
"""Error in selecting correct options for chosen protocol"""
|
18
|
+
|
19
|
+
|
20
|
+
class ShimmingError(HardwareError):
|
21
|
+
"""Specific error in case of poor instrument shimming"""
|
22
|
+
|
23
|
+
|
24
|
+
class RequestError(KeyError):
|
25
|
+
"""Specific error in case of wrong request type"""
|