pychemstation 0.4.7.dev3__py3-none-any.whl → 0.5.1__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.
- .DS_Store +0 -0
- pychemstation/analysis/base_spectrum.py +509 -509
- pychemstation/analysis/spec_utils.py +304 -304
- pychemstation/control/__init__.py +2 -3
- pychemstation/control/comm.py +29 -130
- pychemstation/control/hplc.py +82 -605
- pychemstation/control/table/__init__.py +3 -0
- pychemstation/control/table/method.py +258 -0
- pychemstation/control/table/sequence.py +170 -0
- pychemstation/control/table/table_controller.py +137 -0
- pychemstation/utils/chromatogram.py +6 -5
- pychemstation/utils/macro.py +1 -0
- pychemstation/utils/method_types.py +4 -6
- pychemstation/utils/sequence_types.py +19 -5
- {pychemstation-0.4.7.dev3.dist-info → pychemstation-0.5.1.dist-info}/METADATA +12 -11
- pychemstation-0.5.1.dist-info/RECORD +40 -0
- {pychemstation-0.4.7.dev3.dist-info → pychemstation-0.5.1.dist-info}/top_level.txt +1 -0
- tests/__init__.py +0 -0
- tests/constants.py +12 -0
- tests/test_comm.py +66 -0
- tests/test_method.py +64 -0
- tests/test_sequence.py +102 -0
- pychemstation-0.4.7.dev3.dist-info/RECORD +0 -30
- {pychemstation-0.4.7.dev3.dist-info → pychemstation-0.5.1.dist-info}/LICENSE +0 -0
- {pychemstation-0.4.7.dev3.dist-info → pychemstation-0.5.1.dist-info}/WHEEL +0 -0
pychemstation/control/hplc.py
CHANGED
@@ -1,575 +1,137 @@
|
|
1
1
|
"""
|
2
|
-
Module to provide API for
|
2
|
+
Module to provide API for higher-level HPLC actions.
|
3
3
|
|
4
|
-
|
5
|
-
Answers are received via reply file. On the Chemstation side, a custom
|
6
|
-
Macro monitors the command file, executes commands and writes to the reply file.
|
7
|
-
Each command is given a number (cmd_no) to keep track of which commands have
|
8
|
-
been processed.
|
9
|
-
|
10
|
-
Authors: Alexander Hammer, Hessam Mehr, Lucy Hao
|
4
|
+
Authors: Lucy Hao
|
11
5
|
"""
|
12
6
|
|
13
|
-
import
|
14
|
-
import os
|
15
|
-
import time
|
16
|
-
from typing import Union
|
17
|
-
|
18
|
-
import polling
|
19
|
-
from xsdata.formats.dataclass.parsers import XmlParser
|
7
|
+
from typing import Union, Optional
|
20
8
|
|
21
|
-
from
|
22
|
-
from ..
|
23
|
-
from ..utils.
|
24
|
-
from ..utils.
|
25
|
-
|
26
|
-
|
9
|
+
from ..control import CommunicationController
|
10
|
+
from ..control.table import MethodController, SequenceController
|
11
|
+
from ..utils.chromatogram import AgilentHPLCChromatogram
|
12
|
+
from ..utils.macro import Command, HPLCRunningStatus, HPLCAvailStatus, HPLCErrorStatus
|
13
|
+
from ..utils.method_types import MethodTimetable
|
14
|
+
from ..utils.sequence_types import SequenceTable, SequenceEntry
|
27
15
|
|
28
16
|
|
29
17
|
class HPLCController:
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
self,
|
36
|
-
comm_dir: str,
|
37
|
-
data_dir: str,
|
38
|
-
method_dir: str,
|
39
|
-
sequence_dir: str,
|
40
|
-
cmd_file: str = "cmd",
|
41
|
-
reply_file: str = "reply",
|
42
|
-
):
|
18
|
+
def __init__(self,
|
19
|
+
comm_dir: str,
|
20
|
+
data_dir: str,
|
21
|
+
method_dir: str,
|
22
|
+
sequence_dir: str):
|
43
23
|
"""Initialize HPLC controller. The `hplc_talk.mac` macro file must be loaded in the Chemstation software.
|
44
24
|
`comm_dir` must match the file path in the macro file.
|
45
|
-
|
25
|
+
|
46
26
|
:param comm_dir: Name of directory for communication, where ChemStation will read and write from. Can be any existing directory.
|
47
|
-
:param data_dir: Name of directory that ChemStation saves run data. Must be accessible by ChemStation.
|
48
|
-
:param cmd_file: Name of command file
|
49
|
-
:param reply_file: Name of reply file
|
50
27
|
:raises FileNotFoundError: If either `data_dir`, `method_dir` or `comm_dir` is not a valid directory.
|
51
28
|
"""
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
self.cmd_no = 0
|
56
|
-
else:
|
57
|
-
raise FileNotFoundError(f"comm_dir: {comm_dir} not found.")
|
58
|
-
self._most_recent_hplc_status = None
|
59
|
-
|
60
|
-
if os.path.isdir(data_dir):
|
61
|
-
self.data_dir = data_dir
|
62
|
-
else:
|
63
|
-
raise FileNotFoundError(f"data_dir: {data_dir} not found.")
|
64
|
-
|
65
|
-
if os.path.isdir(method_dir):
|
66
|
-
self.method_dir = method_dir
|
67
|
-
else:
|
68
|
-
raise FileNotFoundError(f"method_dir: {method_dir} not found.")
|
69
|
-
|
70
|
-
if os.path.isdir(sequence_dir):
|
71
|
-
self.sequence_dir = sequence_dir
|
72
|
-
else:
|
73
|
-
raise FileNotFoundError(f"method_dir: {method_dir} not found.")
|
74
|
-
|
75
|
-
self.spectra = {
|
76
|
-
"A": AgilentHPLCChromatogram(self.data_dir),
|
77
|
-
"B": AgilentHPLCChromatogram(self.data_dir),
|
78
|
-
"C": AgilentHPLCChromatogram(self.data_dir),
|
79
|
-
"D": AgilentHPLCChromatogram(self.data_dir),
|
80
|
-
}
|
81
|
-
|
82
|
-
self.data_files: list[str] = []
|
83
|
-
self.internal_variables: list[dict[str, str]] = []
|
84
|
-
|
85
|
-
# Create files for Chemstation to communicate with Python
|
86
|
-
open(self.cmd_file, "a").close()
|
87
|
-
open(self.reply_file, "a").close()
|
88
|
-
|
89
|
-
self.logger = logging.getLogger("hplc_logger")
|
90
|
-
self.logger.addHandler(logging.NullHandler())
|
91
|
-
|
92
|
-
self.reset_cmd_counter()
|
93
|
-
|
94
|
-
self.logger.info("HPLC Controller initialized.")
|
95
|
-
|
96
|
-
def _set_status(self):
|
97
|
-
"""Updates current status of HPLC machine"""
|
98
|
-
self._most_recent_hplc_status = self.status()[0]
|
99
|
-
|
100
|
-
def _check_data_status(self) -> bool:
|
101
|
-
"""Checks if HPLC machine is in an available state, meaning a state that data is not being written.
|
102
|
-
|
103
|
-
:return: whether the HPLC machine is in a safe state to retrieve data back."""
|
104
|
-
old_status = self._most_recent_hplc_status
|
105
|
-
self._set_status()
|
106
|
-
file_exists = os.path.exists(self.data_files[-1]) if len(self.data_files) > 0 else False
|
107
|
-
done_writing_data = isinstance(self._most_recent_hplc_status,
|
108
|
-
HPLCAvailStatus) and old_status != self._most_recent_hplc_status and file_exists
|
109
|
-
return done_writing_data
|
110
|
-
|
111
|
-
def check_hplc_ready_with_data(self) -> bool:
|
112
|
-
"""Checks if ChemStation has finished writing data and can be read back.
|
113
|
-
|
114
|
-
:param method: if you are running a method and want to read back data, the timeout period will be adjusted to be longer than the method's runtime
|
115
|
-
|
116
|
-
:return: Return True if data can be read back, else False.
|
117
|
-
"""
|
118
|
-
self._set_status()
|
119
|
-
|
120
|
-
timeout = 10 * 60
|
121
|
-
hplc_run_done = polling.poll(
|
122
|
-
lambda: self._check_data_status(),
|
123
|
-
timeout=timeout,
|
124
|
-
step=30
|
125
|
-
)
|
126
|
-
|
127
|
-
return hplc_run_done
|
128
|
-
|
129
|
-
def _send(self, cmd: str, cmd_no: int, num_attempts=5) -> None:
|
130
|
-
"""Low-level execution primitive. Sends a command string to HPLC.
|
131
|
-
|
132
|
-
:param cmd: string to be sent to HPLC
|
133
|
-
:param cmd_no: Command number
|
134
|
-
:param num_attempts: Number of attempts to send the command before raising exception.
|
135
|
-
:raises IOError: Could not write to command file.
|
136
|
-
"""
|
137
|
-
err = None
|
138
|
-
for _ in range(num_attempts):
|
139
|
-
time.sleep(1)
|
140
|
-
try:
|
141
|
-
with open(self.cmd_file, "w", encoding="utf8") as cmd_file:
|
142
|
-
cmd_file.write(f"{cmd_no} {cmd}")
|
143
|
-
except IOError as e:
|
144
|
-
err = e
|
145
|
-
self.logger.warning("Failed to send command; trying again.")
|
146
|
-
continue
|
147
|
-
else:
|
148
|
-
self.logger.info("Sent command #%d: %s.", cmd_no, cmd)
|
149
|
-
return
|
150
|
-
else:
|
151
|
-
raise IOError(f"Failed to send command #{cmd_no}: {cmd}.") from err
|
152
|
-
|
153
|
-
def _receive(self, cmd_no: int, num_attempts=100) -> str:
|
154
|
-
"""Low-level execution primitive. Recives a response from HPLC.
|
155
|
-
|
156
|
-
:param cmd_no: Command number
|
157
|
-
:param num_attempts: Number of retries to open reply file
|
158
|
-
:raises IOError: Could not read reply file.
|
159
|
-
:return: ChemStation response
|
160
|
-
"""
|
161
|
-
err = None
|
162
|
-
for _ in range(num_attempts):
|
163
|
-
time.sleep(1)
|
164
|
-
|
165
|
-
try:
|
166
|
-
with open(self.reply_file, "r", encoding="utf_16") as reply_file:
|
167
|
-
response = reply_file.read()
|
168
|
-
except OSError as e:
|
169
|
-
err = e
|
170
|
-
self.logger.warning("Failed to read from reply file; trying again.")
|
171
|
-
continue
|
172
|
-
|
173
|
-
try:
|
174
|
-
first_line = response.splitlines()[0]
|
175
|
-
response_no = int(first_line.split()[0])
|
176
|
-
except IndexError as e:
|
177
|
-
err = e
|
178
|
-
self.logger.warning("Malformed response %s; trying again.", response)
|
179
|
-
continue
|
180
|
-
|
181
|
-
# check that response corresponds to sent command
|
182
|
-
if response_no == cmd_no:
|
183
|
-
self.logger.info("Reply: \n%s", response)
|
184
|
-
return response
|
185
|
-
else:
|
186
|
-
self.logger.warning(
|
187
|
-
"Response #: %d != command #: %d; trying again.",
|
188
|
-
response_no,
|
189
|
-
cmd_no,
|
190
|
-
)
|
191
|
-
continue
|
192
|
-
else:
|
193
|
-
raise IOError(f"Failed to receive reply to command #{cmd_no}.") from err
|
194
|
-
|
195
|
-
def sleepy_send(self, cmd: Union[Command, str]):
|
196
|
-
self.send("Sleep 0.1")
|
197
|
-
self.send(cmd)
|
198
|
-
self.send("Sleep 0.1")
|
29
|
+
self.comm = CommunicationController(comm_dir=comm_dir)
|
30
|
+
self.method_controller = MethodController(controller=self.comm, src=method_dir, data_dir=data_dir)
|
31
|
+
self.sequence_controller = SequenceController(controller=self.comm, src=sequence_dir, data_dir=data_dir)
|
199
32
|
|
200
33
|
def send(self, cmd: Union[Command, str]):
|
201
|
-
|
202
|
-
|
203
|
-
:param cmd: Command to be sent to HPLC
|
204
|
-
"""
|
205
|
-
if self.cmd_no == MAX_CMD_NO:
|
206
|
-
self.reset_cmd_counter()
|
207
|
-
|
208
|
-
cmd_to_send: str = cmd.value if isinstance(cmd, Command) else cmd
|
209
|
-
self.cmd_no += 1
|
210
|
-
self._send(cmd_to_send, self.cmd_no)
|
34
|
+
self.comm.send(cmd)
|
211
35
|
|
212
36
|
def receive(self) -> str:
|
213
|
-
|
214
|
-
|
215
|
-
:return: ChemStation response
|
216
|
-
"""
|
217
|
-
return self._receive(self.cmd_no)
|
218
|
-
|
219
|
-
def reset_cmd_counter(self):
|
220
|
-
"""Resets the command counter."""
|
221
|
-
self._send(Command.RESET_COUNTER_CMD.value, cmd_no=MAX_CMD_NO + 1)
|
222
|
-
self._receive(cmd_no=MAX_CMD_NO + 1)
|
223
|
-
self.cmd_no = 0
|
224
|
-
|
225
|
-
self.logger.debug("Reset command counter")
|
37
|
+
return self.comm.receive()
|
226
38
|
|
227
|
-
def
|
228
|
-
|
39
|
+
def status(self) -> list[HPLCRunningStatus | HPLCAvailStatus | HPLCErrorStatus]:
|
40
|
+
return self.comm.get_status()
|
229
41
|
|
230
|
-
|
42
|
+
def switch_method(self, method_name: str):
|
231
43
|
"""
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
def standby(self):
|
236
|
-
"""Switches all modules in standby mode. All lamps and pumps are switched off."""
|
237
|
-
self.send(Command.STANDBY_CMD)
|
238
|
-
self.logger.debug("Standby command sent.")
|
239
|
-
|
240
|
-
def preprun(self):
|
241
|
-
""" Prepares all modules for run. All lamps and pumps are switched on."""
|
242
|
-
self.send(Command.PREPRUN_CMD)
|
243
|
-
self.logger.debug("PrepRun command sent.")
|
244
|
-
|
245
|
-
def status(self) -> list[Union[HPLCRunningStatus, HPLCAvailStatus, HPLCErrorStatus]]:
|
246
|
-
"""Get device status(es).
|
44
|
+
Allows the user to switch between pre-programmed methods. No need to append '.M'
|
45
|
+
to the end of the method name. For example. for the method named 'General-Poroshell.M',
|
46
|
+
only 'General-Poroshell' is needed.
|
247
47
|
|
248
|
-
:
|
48
|
+
:param method_name: any available method in Chemstation method directory
|
49
|
+
:raise IndexError: Response did not have expected format. Try again.
|
50
|
+
:raise AssertionError: The desired method is not selected. Try again.
|
249
51
|
"""
|
250
|
-
self.
|
251
|
-
time.sleep(1)
|
252
|
-
|
253
|
-
try:
|
254
|
-
parsed_response = self.receive().splitlines()[1].split()[1:]
|
255
|
-
except IOError:
|
256
|
-
return [HPLCErrorStatus.NORESPONSE]
|
257
|
-
except IndexError:
|
258
|
-
return [HPLCErrorStatus.MALFORMED]
|
259
|
-
recieved_status = [str_to_status(res) for res in parsed_response]
|
260
|
-
self._most_recent_hplc_status = recieved_status[0]
|
261
|
-
return recieved_status
|
52
|
+
self.method_controller.switch(method_name)
|
262
53
|
|
263
|
-
def
|
264
|
-
"""Stops Macro execution. Connection will be lost."""
|
265
|
-
self.send(Command.STOP_MACRO_CMD)
|
266
|
-
|
267
|
-
def add_table_row(self, table: Table):
|
268
|
-
"""Adds a row to the provided table for currently loaded method or sequence.
|
269
|
-
Import either the SEQUENCE_TABLE or METHOD_TIMETABLE from hein_analytical_control.constants.
|
270
|
-
You can also provide your own table.
|
271
|
-
|
272
|
-
:param table: the table to add a new row to
|
54
|
+
def switch_sequence(self, sequence_name: str):
|
273
55
|
"""
|
274
|
-
|
56
|
+
Allows the user to switch between pre-programmed sequences. The sequence name does not need the '.S' extension.
|
57
|
+
For example. for the method named 'mySeq.S', only 'mySeq' is needed.
|
275
58
|
|
276
|
-
|
277
|
-
"""Deletes the table for the current loaded method or sequence.
|
278
|
-
Import either the SEQUENCE_TABLE or METHOD_TIMETABLE from hein_analytical_control.constants.
|
279
|
-
You can also provide your own table.
|
280
|
-
|
281
|
-
:param table: the table to delete
|
59
|
+
:param seq_name: The name of the sequence file
|
282
60
|
"""
|
283
|
-
self.
|
61
|
+
self.sequence_controller.switch(sequence_name)
|
284
62
|
|
285
|
-
def
|
286
|
-
"""
|
287
|
-
|
288
|
-
|
63
|
+
def run_method(self, experiment_name: str):
|
64
|
+
"""
|
65
|
+
This is the preferred method to trigger a run.
|
66
|
+
Starts the currently selected method, storing data
|
67
|
+
under the <data_dir>/<experiment_name>.D folder.
|
68
|
+
The <experiment_name> will be appended with a timestamp in the '%Y-%m-%d-%H-%M' format.
|
69
|
+
Device must be ready.
|
289
70
|
|
290
|
-
:param
|
71
|
+
:param experiment_name: Name of the experiment
|
291
72
|
"""
|
292
|
-
self.
|
73
|
+
self.method_controller.run(experiment_name)
|
293
74
|
|
294
|
-
def
|
295
|
-
"""Updates the currently loaded sequence table with the provided table. This method will delete the existing sequence table and remake it.
|
296
|
-
If you would only like to edit a single row of a sequence table, use `edit_sequence_table_row` instead.
|
297
|
-
:param sequence_table:
|
75
|
+
def run_sequence(self, sequence_table: SequenceTable):
|
298
76
|
"""
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
self.sleep(1)
|
303
|
-
self.new_table(SEQUENCE_TABLE)
|
304
|
-
self.sleep(1)
|
305
|
-
self._get_table_rows(SEQUENCE_TABLE)
|
306
|
-
for i, row in enumerate(sequence_table.rows):
|
307
|
-
self.add_table_row(SEQUENCE_TABLE)
|
308
|
-
self.sleep(1)
|
309
|
-
self.send(Command.SAVE_SEQUENCE_CMD)
|
310
|
-
self.sleep(1)
|
311
|
-
self._get_table_rows(SEQUENCE_TABLE)
|
312
|
-
self.send(Command.SWITCH_SEQUENCE_CMD)
|
313
|
-
for i, row in enumerate(sequence_table.rows):
|
314
|
-
self.edit_sequence_table_row(row=row, row_num=i + 1)
|
315
|
-
self.sleep(1)
|
316
|
-
self.send(Command.SAVE_SEQUENCE_CMD)
|
317
|
-
self.sleep(1)
|
77
|
+
Starts the currently loaded sequence, storing data
|
78
|
+
under the <data_dir>/<sequence table name> folder.
|
79
|
+
Device must be ready.
|
318
80
|
|
319
|
-
|
320
|
-
"""Edits a row in the sequence table. Assumes the row already exists. If not, call `add_sequence_table_row`.
|
321
|
-
:param row: sequence row entry with updated information
|
322
|
-
:param row_num: the row to edit, based on 1-based indexing
|
81
|
+
:param sequence_table:
|
323
82
|
"""
|
324
|
-
|
325
|
-
self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(register=SEQUENCE_TABLE.register,
|
326
|
-
table_name=SEQUENCE_TABLE.name,
|
327
|
-
row=row_num,
|
328
|
-
col_name=RegisterFlag.NAME,
|
329
|
-
val=row.name))
|
330
|
-
if row.vial_location:
|
331
|
-
self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(register=SEQUENCE_TABLE.register,
|
332
|
-
table_name=SEQUENCE_TABLE.name,
|
333
|
-
row=row_num,
|
334
|
-
col_name=RegisterFlag.VIAL_LOCATION,
|
335
|
-
val=row.vial_location))
|
336
|
-
if row.method:
|
337
|
-
self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(register=SEQUENCE_TABLE.register,
|
338
|
-
table_name=SEQUENCE_TABLE.name,
|
339
|
-
row=row_num,
|
340
|
-
col_name=RegisterFlag.NAME,
|
341
|
-
val=row.method))
|
342
|
-
if row.inj_vol:
|
343
|
-
self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(register=SEQUENCE_TABLE.register,
|
344
|
-
table_name=SEQUENCE_TABLE.name,
|
345
|
-
row=row_num,
|
346
|
-
col_name=RegisterFlag.NAME,
|
347
|
-
val=row.inj_vol))
|
83
|
+
self.sequence_controller.run(sequence_table)
|
348
84
|
|
349
85
|
def edit_method(self, updated_method: MethodTimetable):
|
350
86
|
"""Updated the currently loaded method in ChemStation with provided values.
|
351
87
|
|
352
88
|
:param updated_method: the method with updated values, to be sent to Chemstation to modify the currently loaded method.
|
353
89
|
"""
|
354
|
-
|
355
|
-
max_time: Param = updated_method.first_row.maximum_run_time
|
356
|
-
temp: Param = updated_method.first_row.temperature
|
357
|
-
injvol: Param = updated_method.first_row.inj_vol
|
358
|
-
equalib_time: Param = updated_method.first_row.equ_time
|
359
|
-
flow: Param = updated_method.first_row.flow
|
360
|
-
|
361
|
-
# Method settings required for all runs
|
362
|
-
self.send(TableOperation.DELETE_TABLE.value.format(register=METHOD_TIMETABLE.register,
|
363
|
-
table_name=METHOD_TIMETABLE.name, ))
|
364
|
-
self._update_method_param(initial_organic_modifier)
|
365
|
-
self._update_method_param(flow)
|
366
|
-
self._update_method_param(Param(val="Set",
|
367
|
-
chemstation_key=RegisterFlag.STOPTIME_MODE,
|
368
|
-
ptype=PType.STR))
|
369
|
-
self._update_method_param(max_time)
|
370
|
-
self._update_method_param(Param(val="Off",
|
371
|
-
chemstation_key=RegisterFlag.POSTIME_MODE,
|
372
|
-
ptype=PType.STR))
|
373
|
-
|
374
|
-
# TODO: set the oven temperature
|
375
|
-
# self._hplc_controller.update_method_param("str", "Set", "TemperatureControl_Mode")
|
376
|
-
# self._hplc_controller.update_method_param("num", temp, temp.chemstation_key[0])
|
377
|
-
# self._hplc_controller.update_method_param("num", temp, temp.chemstation_key[1])
|
378
|
-
# self._hplc_controller.update_method_param("str", "Set", "TemperatureControl2_Mode")
|
379
|
-
# self._hplc_controller.send("DownloadRCMethod PMP1")
|
90
|
+
self.method_controller.edit(updated_method)
|
380
91
|
|
381
|
-
|
382
|
-
|
383
|
-
self._update_method_timetable(updated_method.subsequent_rows)
|
384
|
-
|
385
|
-
def _get_table_rows(self, table: Table) -> str:
|
386
|
-
self.send(TableOperation.GET_OBJ_HDR_VAL.value.format(internal_val="Row",
|
387
|
-
register=table.register,
|
388
|
-
table_name=table.name,
|
389
|
-
col_name=RegisterFlag.NUM_ROWS, ))
|
390
|
-
res = self.receive()
|
391
|
-
self.send("Sleep 1")
|
392
|
-
self.send('Print Rows')
|
393
|
-
return res
|
394
|
-
|
395
|
-
def _update_method_timetable(self, timetable_rows: list[Entry]):
|
396
|
-
"""Updates the timetable, which is seen when right-clicking the pump GUI in Chemstation to get to the timetable.
|
397
|
-
:param updated_method:
|
92
|
+
def edit_sequence(self, updated_sequence: SequenceTable):
|
398
93
|
"""
|
399
|
-
|
400
|
-
|
401
|
-
self.delete_table(METHOD_TIMETABLE)
|
402
|
-
res = self._get_table_rows(METHOD_TIMETABLE)
|
94
|
+
Updates the currently loaded sequence table with the provided table. This method will delete the existing sequence table and remake it.
|
95
|
+
If you would only like to edit a single row of a sequence table, use `edit_sequence_row` instead.
|
403
96
|
|
404
|
-
|
405
|
-
self.delete_table(METHOD_TIMETABLE)
|
406
|
-
res = self._get_table_rows(METHOD_TIMETABLE)
|
407
|
-
self.new_table(METHOD_TIMETABLE)
|
408
|
-
self._get_table_rows(METHOD_TIMETABLE)
|
409
|
-
|
410
|
-
for i, row in enumerate(timetable_rows):
|
411
|
-
self.send("Sleep 1")
|
412
|
-
self.send(TableOperation.NEW_ROW.value.format(register=METHOD_TIMETABLE.register,
|
413
|
-
table_name=METHOD_TIMETABLE.name))
|
414
|
-
self.send("Sleep 1")
|
415
|
-
|
416
|
-
if i == 0:
|
417
|
-
self.sleepy_send(TableOperation.NEW_COL_TEXT.value.format(
|
418
|
-
register=METHOD_TIMETABLE.register,
|
419
|
-
table_name=METHOD_TIMETABLE.name,
|
420
|
-
col_name="Function",
|
421
|
-
val=RegisterFlag.SOLVENT_COMPOSITION.value))
|
422
|
-
self.sleepy_send(TableOperation.NEW_COL_VAL.value.format(
|
423
|
-
register=METHOD_TIMETABLE.register,
|
424
|
-
table_name=METHOD_TIMETABLE.name,
|
425
|
-
col_name="Time",
|
426
|
-
val=row.start_time))
|
427
|
-
self.sleepy_send(TableOperation.NEW_COL_VAL.value.format(
|
428
|
-
register=METHOD_TIMETABLE.register,
|
429
|
-
table_name=METHOD_TIMETABLE.name,
|
430
|
-
col_name=RegisterFlag.SOLVENT_B_COMPOSITION.value,
|
431
|
-
val=row.organic_modifer))
|
432
|
-
else:
|
433
|
-
self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(
|
434
|
-
register=METHOD_TIMETABLE.register,
|
435
|
-
table_name=METHOD_TIMETABLE.name,
|
436
|
-
row="Rows",
|
437
|
-
col_name="Function",
|
438
|
-
val=RegisterFlag.SOLVENT_COMPOSITION.value))
|
439
|
-
self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(
|
440
|
-
register=METHOD_TIMETABLE.register,
|
441
|
-
table_name=METHOD_TIMETABLE.name,
|
442
|
-
row="Rows",
|
443
|
-
col_name="Time",
|
444
|
-
val=row.start_time))
|
445
|
-
self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(
|
446
|
-
register=METHOD_TIMETABLE.register,
|
447
|
-
table_name=METHOD_TIMETABLE.name,
|
448
|
-
row="Rows",
|
449
|
-
col_name=RegisterFlag.SOLVENT_B_COMPOSITION.value,
|
450
|
-
val=row.organic_modifer))
|
451
|
-
|
452
|
-
self.send("Sleep 1")
|
453
|
-
self.send("DownloadRCMethod PMP1")
|
454
|
-
self.send("Sleep 1")
|
455
|
-
|
456
|
-
def _update_method_param(self, method_param: Param):
|
457
|
-
"""Change a method parameter, changes what is visibly seen in Chemstation GUI. (changes the first row in the timetable)
|
458
|
-
|
459
|
-
:param method_param: a parameter to update for currently loaded method.
|
97
|
+
:param sequence_table:
|
460
98
|
"""
|
461
|
-
|
462
|
-
setting_command = TableOperation.UPDATE_OBJ_HDR_VAL if method_param.ptype == PType.NUM else TableOperation.UPDATE_OBJ_HDR_TEXT
|
463
|
-
if isinstance(method_param.chemstation_key, list):
|
464
|
-
for register_flag in method_param.chemstation_key:
|
465
|
-
self.send(setting_command.value.format(register=register,
|
466
|
-
register_flag=register_flag,
|
467
|
-
val=method_param.val))
|
468
|
-
else:
|
469
|
-
self.send(setting_command.value.format(register=register,
|
470
|
-
register_flag=method_param.chemstation_key,
|
471
|
-
val=method_param.val))
|
472
|
-
time.sleep(2)
|
99
|
+
self.sequence_controller.edit(updated_sequence)
|
473
100
|
|
474
|
-
def
|
475
|
-
"""Checks if a given method is already loaded into Chemstation. Method name does not need the ".M" extension.
|
476
|
-
|
477
|
-
:param method_name: a Chemstation method
|
478
|
-
:return: True if method is already loaded
|
101
|
+
def edit_sequence_row(self, row: SequenceEntry, num: int):
|
479
102
|
"""
|
480
|
-
|
481
|
-
parsed_response = self.receive().splitlines()[1].split()[1:][0]
|
482
|
-
return method_name in parsed_response
|
483
|
-
|
484
|
-
def switch_method(self, method_name: str):
|
485
|
-
"""Allows the user to switch between pre-programmed methods. No need to append '.M'
|
486
|
-
to the end of the method name. For example. for the method named 'General-Poroshell.M',
|
487
|
-
only 'General-Poroshell' is needed.
|
103
|
+
Edits a row in the sequence table. Assumes the row already exists.
|
488
104
|
|
489
|
-
:param
|
490
|
-
:
|
491
|
-
:raise AssertionError: The desired method is not selected. Try again.
|
105
|
+
:param row: sequence row entry with updated information
|
106
|
+
:param row_num: the row to edit, based on -1-based indexing
|
492
107
|
"""
|
493
|
-
self.
|
494
|
-
Command.SWITCH_METHOD_CMD.value.format(method_dir=self.method_dir, method_name=method_name)
|
495
|
-
)
|
496
|
-
|
497
|
-
time.sleep(2)
|
498
|
-
self.send(Command.GET_METHOD_CMD)
|
499
|
-
time.sleep(2)
|
500
|
-
# check that method switched
|
501
|
-
for _ in range(10):
|
502
|
-
try:
|
503
|
-
parsed_response = self.receive().splitlines()[1].split()[1:][0]
|
504
|
-
break
|
505
|
-
except IndexError:
|
506
|
-
self.logger.debug("Malformed response. Trying again.")
|
507
|
-
continue
|
508
|
-
|
509
|
-
assert parsed_response == f"{method_name}.M", "Switching Methods failed."
|
108
|
+
self.sequence_controller.edit_row(row, num)
|
510
109
|
|
511
|
-
def
|
512
|
-
"""Retrieve method details of an existing method. Don't need to append ".M" to the end. This assumes the
|
513
|
-
organic modifier is in Channel B and that Channel A contains the aq layer. Additionally, assumes
|
514
|
-
only two solvents are being used.
|
515
|
-
|
516
|
-
:param method_name: name of method to load details of
|
517
|
-
:raises FileNotFoundError: Method does not exist
|
518
|
-
:return: method details
|
110
|
+
def get_last_run_method_data(self) -> Optional[dict[str, AgilentHPLCChromatogram]]:
|
519
111
|
"""
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
method = parser.parse(method_path, PumpMethod)
|
527
|
-
dad = parser.parse(dad_path, DadMethod)
|
112
|
+
Returns the last run method data.
|
113
|
+
"""
|
114
|
+
data_valid, data = self.method_controller.get_data()
|
115
|
+
if data_valid:
|
116
|
+
return data
|
117
|
+
return None
|
528
118
|
|
529
|
-
|
530
|
-
|
119
|
+
def get_last_run_sequence_data(self) -> Optional[list[dict[str, AgilentHPLCChromatogram]]]:
|
120
|
+
"""
|
121
|
+
Returns data for all rows in the last run sequence data.
|
122
|
+
"""
|
123
|
+
data_valid, data = self.sequence_controller.get_data()
|
124
|
+
if data_valid:
|
125
|
+
return data
|
126
|
+
return None
|
531
127
|
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
aq_modifier = solvent
|
536
|
-
elif solvent.channel == "Channel_B":
|
537
|
-
organic_modifier = solvent
|
128
|
+
def standby(self):
|
129
|
+
"""Switches all modules in standby mode. All lamps and pumps are switched off."""
|
130
|
+
self.send(Command.STANDBY_CMD)
|
538
131
|
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
chemstation_key=RegisterFlag.SOLVENT_B_COMPOSITION,
|
543
|
-
ptype=PType.NUM),
|
544
|
-
flow=Param(val=method.flow,
|
545
|
-
chemstation_key=RegisterFlag.FLOW,
|
546
|
-
ptype=PType.NUM),
|
547
|
-
maximum_run_time=Param(val=method.stop_time,
|
548
|
-
chemstation_key=RegisterFlag.MAX_TIME,
|
549
|
-
ptype=PType.NUM),
|
550
|
-
temperature=Param(val=None,
|
551
|
-
chemstation_key=[RegisterFlag.COLUMN_OVEN_TEMP1,
|
552
|
-
RegisterFlag.COLUMN_OVEN_TEMP2],
|
553
|
-
ptype=PType.NUM),
|
554
|
-
inj_vol=Param(val=None,
|
555
|
-
chemstation_key=None,
|
556
|
-
ptype=PType.NUM),
|
557
|
-
equ_time=Param(val=None,
|
558
|
-
chemstation_key=None,
|
559
|
-
ptype=PType.NUM)),
|
560
|
-
subsequent_rows=[
|
561
|
-
Entry(
|
562
|
-
start_time=tte.time,
|
563
|
-
organic_modifer=tte.percent_b,
|
564
|
-
flow=method.flow
|
565
|
-
) for tte in method.timetable.timetable_entry
|
566
|
-
],
|
567
|
-
dad_wavelengthes=dad.signals.signal,
|
568
|
-
organic_modifier=organic_modifier,
|
569
|
-
modifier_a=aq_modifier
|
570
|
-
)
|
571
|
-
else:
|
572
|
-
raise FileNotFoundError
|
132
|
+
def preprun(self):
|
133
|
+
""" Prepares all modules for run. All lamps and pumps are switched on."""
|
134
|
+
self.send(Command.PREPRUN_CMD)
|
573
135
|
|
574
136
|
def lamp_on(self):
|
575
137
|
"""Turns the UV lamp on."""
|
@@ -586,88 +148,3 @@ class HPLCController:
|
|
586
148
|
def pump_off(self):
|
587
149
|
"""Turns the pump off."""
|
588
150
|
self.send(Command.PUMP_OFF_CMD)
|
589
|
-
|
590
|
-
def switch_sequence(self, seq_name: str):
|
591
|
-
"""Switch to the specified sequence. The sequence name does not need the '.S' extension.
|
592
|
-
:param seq_name: The name of the sequence file
|
593
|
-
:param sequence_dir: The directory where the sequence file resides
|
594
|
-
"""
|
595
|
-
self.send(f'_SeqFile$ = "{seq_name}.S"')
|
596
|
-
self.send(f'_SeqPath$ = "{self.sequence_dir}"')
|
597
|
-
self.send(Command.SWITCH_SEQUENCE_CMD)
|
598
|
-
|
599
|
-
time.sleep(2)
|
600
|
-
self.send(Command.GET_SEQUENCE_CMD)
|
601
|
-
time.sleep(2)
|
602
|
-
# check that method switched
|
603
|
-
for _ in range(10):
|
604
|
-
try:
|
605
|
-
parsed_response = self.receive().splitlines()[1].split()[1:][0]
|
606
|
-
break
|
607
|
-
except IndexError:
|
608
|
-
self.logger.debug("Malformed response. Trying again.")
|
609
|
-
continue
|
610
|
-
|
611
|
-
assert parsed_response == f"{seq_name}.S", "Switching sequence failed."
|
612
|
-
|
613
|
-
def run_sequence(self, experiment_name: str):
|
614
|
-
"""Starts the currently loaded sequence, storing data
|
615
|
-
under the <data_dir>/<experiment_name>.D folder.
|
616
|
-
The <experiment_name> will be appended with a timestamp in the '%Y-%m-%d-%H-%M' format.
|
617
|
-
Device must be ready.
|
618
|
-
|
619
|
-
:param experiment_name: Name of the experiment
|
620
|
-
"""
|
621
|
-
timestamp = time.strftime(TIME_FORMAT)
|
622
|
-
|
623
|
-
self.send(
|
624
|
-
Command.RUN_SEQUENCE_CMD.value.format(
|
625
|
-
data_dir=self.data_dir, experiment_name=experiment_name, timestamp=timestamp
|
626
|
-
)
|
627
|
-
)
|
628
|
-
|
629
|
-
folder_name = f"{experiment_name}_{timestamp}.D"
|
630
|
-
self.data_files.append(os.path.join(self.data_dir, folder_name))
|
631
|
-
self.logger.info("Started HPLC run: %s.", folder_name)
|
632
|
-
|
633
|
-
def start_method(self):
|
634
|
-
"""Starts and executes currently loaded method to run according to Run Time Checklist. Device must be ready.
|
635
|
-
Does not store the folder where data is saved.
|
636
|
-
"""
|
637
|
-
self.send(Command.START_METHOD_CMD)
|
638
|
-
|
639
|
-
def run_method(self, experiment_name: str):
|
640
|
-
"""This is the preferred method to trigger a run.
|
641
|
-
Starts the currently selected method, storing data
|
642
|
-
under the <data_dir>/<experiment_name>.D folder.
|
643
|
-
The <experiment_name> will be appended with a timestamp in the '%Y-%m-%d-%H-%M' format.
|
644
|
-
Device must be ready.
|
645
|
-
|
646
|
-
:param experiment_name: Name of the experiment
|
647
|
-
"""
|
648
|
-
timestamp = time.strftime(TIME_FORMAT)
|
649
|
-
|
650
|
-
self.send(
|
651
|
-
Command.RUN_METHOD_CMD.value.format(
|
652
|
-
data_dir=self.data_dir, experiment_name=experiment_name, timestamp=timestamp
|
653
|
-
)
|
654
|
-
)
|
655
|
-
|
656
|
-
folder_name = f"{experiment_name}_{timestamp}.D"
|
657
|
-
self.data_files.append(os.path.join(self.data_dir, folder_name))
|
658
|
-
self.logger.info("Started HPLC run: %s.", folder_name)
|
659
|
-
|
660
|
-
def stop_method(self):
|
661
|
-
"""Stops the run. A dialog window will pop up and manual intervention may be required."""
|
662
|
-
self.send(Command.STOP_METHOD_CMD)
|
663
|
-
|
664
|
-
def get_spectrum(self):
|
665
|
-
""" Load last chromatogram for any channel in spectra dictionary."""
|
666
|
-
last_file = self.data_files[-1] if len(self.data_files) > 0 else None
|
667
|
-
|
668
|
-
if last_file is None:
|
669
|
-
raise IndexError
|
670
|
-
|
671
|
-
for channel, spec in self.spectra.items():
|
672
|
-
spec.load_spectrum(data_path=last_file, channel=channel)
|
673
|
-
self.logger.info("%s chromatogram loaded.", channel)
|