pychemstation 0.7.0.dev2__py3-none-any.whl → 0.8.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.
- pychemstation/analysis/base_spectrum.py +3 -6
- pychemstation/analysis/process_report.py +248 -225
- pychemstation/analysis/utils.py +3 -1
- pychemstation/control/README.md +124 -0
- pychemstation/control/controllers/README.md +1 -0
- pychemstation/control/controllers/__init__.py +0 -2
- pychemstation/control/controllers/comm.py +27 -20
- pychemstation/control/controllers/devices/device.py +17 -4
- pychemstation/control/controllers/tables/method.py +57 -39
- pychemstation/control/controllers/tables/sequence.py +98 -28
- pychemstation/control/controllers/tables/table.py +124 -127
- pychemstation/control/hplc.py +82 -37
- pychemstation/generated/dad_method.py +3 -3
- pychemstation/generated/pump_method.py +7 -7
- pychemstation/out.txt +145 -0
- pychemstation/tests.ipynb +310 -0
- pychemstation/utils/chromatogram.py +5 -1
- pychemstation/utils/injector_types.py +2 -2
- pychemstation/utils/macro.py +1 -1
- pychemstation/utils/table_types.py +3 -0
- pychemstation/utils/tray_types.py +59 -39
- {pychemstation-0.7.0.dev2.dist-info → pychemstation-0.8.1.dist-info}/METADATA +25 -21
- pychemstation-0.8.1.dist-info/RECORD +39 -0
- {pychemstation-0.7.0.dev2.dist-info → pychemstation-0.8.1.dist-info}/WHEEL +1 -2
- pychemstation/control/comm.py +0 -206
- pychemstation/control/controllers/devices/column.py +0 -12
- pychemstation/control/controllers/devices/dad.py +0 -0
- pychemstation/control/controllers/devices/pump.py +0 -43
- pychemstation/control/controllers/method.py +0 -338
- pychemstation/control/controllers/sequence.py +0 -190
- pychemstation/control/controllers/table_controller.py +0 -266
- pychemstation/control/table/__init__.py +0 -3
- pychemstation/control/table/method.py +0 -274
- pychemstation/control/table/sequence.py +0 -210
- pychemstation/control/table/table_controller.py +0 -201
- pychemstation-0.7.0.dev2.dist-info/RECORD +0 -58
- pychemstation-0.7.0.dev2.dist-info/top_level.txt +0 -2
- tests/__init__.py +0 -0
- tests/constants.py +0 -88
- tests/test_comb.py +0 -136
- tests/test_comm.py +0 -65
- tests/test_inj.py +0 -39
- tests/test_method.py +0 -99
- tests/test_nightly.py +0 -80
- tests/test_proc_rep.py +0 -52
- tests/test_runs_stable.py +0 -125
- tests/test_sequence.py +0 -125
- tests/test_stable.py +0 -276
- {pychemstation-0.7.0.dev2.dist-info → pychemstation-0.8.1.dist-info/licenses}/LICENSE +0 -0
@@ -5,32 +5,26 @@ Authors: Lucy Hao
|
|
5
5
|
"""
|
6
6
|
|
7
7
|
import abc
|
8
|
+
import math
|
8
9
|
import os
|
9
|
-
import
|
10
|
-
from
|
11
|
-
from typing import Union, Optional, AnyStr, List
|
10
|
+
import time
|
11
|
+
from typing import Union, Optional, List, Tuple, Dict
|
12
12
|
|
13
|
-
import numpy as np
|
14
13
|
import polling
|
15
14
|
import rainbow as rb
|
16
|
-
from result import Result,
|
15
|
+
from result import Result, Err
|
17
16
|
|
17
|
+
from ....analysis.process_report import AgilentReport, ReportType, CSVProcessor, TXTProcessor
|
18
18
|
from ....control.controllers.comm import CommunicationController
|
19
19
|
from ....utils.chromatogram import AgilentHPLCChromatogram, AgilentChannelChromatogramData
|
20
20
|
from ....utils.macro import Command, HPLCRunningStatus, Response
|
21
21
|
from ....utils.method_types import MethodDetails
|
22
22
|
from ....utils.sequence_types import SequenceDataFiles, SequenceTable
|
23
|
-
from ....utils.table_types import Table, TableOperation, RegisterFlag
|
23
|
+
from ....utils.table_types import Table, TableOperation, RegisterFlag, T
|
24
24
|
|
25
25
|
TableType = Union[MethodDetails, SequenceTable]
|
26
26
|
|
27
27
|
|
28
|
-
@dataclass
|
29
|
-
class ChromData:
|
30
|
-
x: np.array
|
31
|
-
y: np.array
|
32
|
-
|
33
|
-
|
34
28
|
class TableController(abc.ABC):
|
35
29
|
|
36
30
|
def __init__(self, controller: CommunicationController,
|
@@ -39,36 +33,36 @@ class TableController(abc.ABC):
|
|
39
33
|
table: Table,
|
40
34
|
offline: bool = False):
|
41
35
|
self.controller = controller
|
42
|
-
self.
|
36
|
+
self.table_locator = table
|
43
37
|
self.table_state: Optional[TableType] = None
|
38
|
+
self.curr_run_starting_time: Optional[float] = None
|
39
|
+
self.timeout: Optional[float] = None
|
44
40
|
|
45
41
|
if not offline:
|
46
|
-
|
47
|
-
self.send('Local Rows')
|
48
|
-
|
49
|
-
if src and os.path.isdir(src):
|
50
|
-
self.src: str = src
|
51
|
-
elif isinstance(src, str):
|
42
|
+
if src and not os.path.isdir(src):
|
52
43
|
raise FileNotFoundError(f"dir: {src} not found.")
|
53
44
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
self.
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
45
|
+
for d in data_dirs:
|
46
|
+
if not os.path.isdir(d):
|
47
|
+
raise FileNotFoundError(f"dir: {d} not found.")
|
48
|
+
if r"\\" in d:
|
49
|
+
raise ValueError("Data directories should not be raw strings!")
|
50
|
+
self.src: str = src
|
51
|
+
self.data_dirs: List[str] = data_dirs
|
52
|
+
|
53
|
+
self.spectra: dict[str, Optional[AgilentHPLCChromatogram]] = {
|
54
|
+
"A": AgilentHPLCChromatogram(),
|
55
|
+
"B": AgilentHPLCChromatogram(),
|
56
|
+
"C": AgilentHPLCChromatogram(),
|
57
|
+
"D": AgilentHPLCChromatogram(),
|
58
|
+
"E": AgilentHPLCChromatogram(),
|
59
|
+
"F": AgilentHPLCChromatogram(),
|
60
|
+
"G": AgilentHPLCChromatogram(),
|
61
|
+
"H": AgilentHPLCChromatogram(),
|
62
|
+
}
|
63
|
+
self.report: Optional[AgilentReport] = None
|
64
|
+
self.uv: Dict[str, AgilentHPLCChromatogram] = {}
|
65
|
+
self.data_files: List = []
|
72
66
|
|
73
67
|
def receive(self) -> Result[Response, str]:
|
74
68
|
for _ in range(10):
|
@@ -96,23 +90,24 @@ class TableController(abc.ABC):
|
|
96
90
|
self.send(Command.SLEEP_CMD.value.format(seconds=seconds))
|
97
91
|
|
98
92
|
def get_num(self, row: int, col_name: RegisterFlag) -> Union[int, float]:
|
99
|
-
return self.controller.get_num_val(TableOperation.GET_ROW_VAL.value.format(register=self.
|
100
|
-
table_name=self.
|
93
|
+
return self.controller.get_num_val(TableOperation.GET_ROW_VAL.value.format(register=self.table_locator.register,
|
94
|
+
table_name=self.table_locator.name,
|
101
95
|
row=row,
|
102
96
|
col_name=col_name.value))
|
103
97
|
|
104
98
|
def get_text(self, row: int, col_name: RegisterFlag) -> str:
|
105
|
-
return self.controller.get_text_val(
|
106
|
-
|
107
|
-
|
108
|
-
|
99
|
+
return self.controller.get_text_val(
|
100
|
+
TableOperation.GET_ROW_TEXT.value.format(register=self.table_locator.register,
|
101
|
+
table_name=self.table_locator.name,
|
102
|
+
row=row,
|
103
|
+
col_name=col_name.value))
|
109
104
|
|
110
105
|
def add_new_col_num(self,
|
111
106
|
col_name: RegisterFlag,
|
112
107
|
val: Union[int, float]):
|
113
108
|
self.sleepy_send(TableOperation.NEW_COL_VAL.value.format(
|
114
|
-
register=self.
|
115
|
-
table_name=self.
|
109
|
+
register=self.table_locator.register,
|
110
|
+
table_name=self.table_locator.name,
|
116
111
|
col_name=col_name,
|
117
112
|
val=val))
|
118
113
|
|
@@ -120,8 +115,8 @@ class TableController(abc.ABC):
|
|
120
115
|
col_name: RegisterFlag,
|
121
116
|
val: str):
|
122
117
|
self.sleepy_send(TableOperation.NEW_COL_TEXT.value.format(
|
123
|
-
register=self.
|
124
|
-
table_name=self.
|
118
|
+
register=self.table_locator.register,
|
119
|
+
table_name=self.table_locator.name,
|
125
120
|
col_name=col_name,
|
126
121
|
val=val))
|
127
122
|
|
@@ -130,8 +125,8 @@ class TableController(abc.ABC):
|
|
130
125
|
val: Union[int, float],
|
131
126
|
row: Optional[int] = None):
|
132
127
|
self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(
|
133
|
-
register=self.
|
134
|
-
table_name=self.
|
128
|
+
register=self.table_locator.register,
|
129
|
+
table_name=self.table_locator.name,
|
135
130
|
row=row if row is not None else 'Rows',
|
136
131
|
col_name=col_name,
|
137
132
|
val=val))
|
@@ -141,8 +136,8 @@ class TableController(abc.ABC):
|
|
141
136
|
val: str,
|
142
137
|
row: Optional[int] = None):
|
143
138
|
self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(
|
144
|
-
register=self.
|
145
|
-
table_name=self.
|
139
|
+
register=self.table_locator.register,
|
140
|
+
table_name=self.table_locator.name,
|
146
141
|
row=row if row is not None else 'Rows',
|
147
142
|
col_name=col_name,
|
148
143
|
val=val))
|
@@ -152,48 +147,37 @@ class TableController(abc.ABC):
|
|
152
147
|
pass
|
153
148
|
|
154
149
|
def delete_row(self, row: int):
|
155
|
-
self.sleepy_send(TableOperation.DELETE_ROW.value.format(register=self.
|
156
|
-
table_name=self.
|
150
|
+
self.sleepy_send(TableOperation.DELETE_ROW.value.format(register=self.table_locator.register,
|
151
|
+
table_name=self.table_locator.name,
|
157
152
|
row=row))
|
158
153
|
|
159
154
|
def add_row(self):
|
160
155
|
"""
|
161
156
|
Adds a row to the provided table for currently loaded method or sequence.
|
162
|
-
Import either the SEQUENCE_TABLE or METHOD_TIMETABLE from hein_analytical_control.constants.
|
163
|
-
You can also provide your own table.
|
164
|
-
|
165
|
-
:param table: the table to add a new row to
|
166
157
|
"""
|
167
|
-
self.sleepy_send(TableOperation.NEW_ROW.value.format(register=self.
|
168
|
-
table_name=self.
|
158
|
+
self.sleepy_send(TableOperation.NEW_ROW.value.format(register=self.table_locator.register,
|
159
|
+
table_name=self.table_locator.name))
|
169
160
|
|
170
161
|
def delete_table(self):
|
171
162
|
"""
|
172
163
|
Deletes the table for the current loaded method or sequence.
|
173
|
-
Import either the SEQUENCE_TABLE or METHOD_TIMETABLE from hein_analytical_control.constants.
|
174
|
-
You can also provide your own table.
|
175
|
-
|
176
|
-
:param table: the table to delete
|
177
164
|
"""
|
178
|
-
self.sleepy_send(TableOperation.DELETE_TABLE.value.format(register=self.
|
179
|
-
table_name=self.
|
165
|
+
self.sleepy_send(TableOperation.DELETE_TABLE.value.format(register=self.table_locator.register,
|
166
|
+
table_name=self.table_locator.name))
|
180
167
|
|
181
168
|
def new_table(self):
|
182
169
|
"""
|
183
|
-
Creates the table for the currently loaded method or sequence.
|
184
|
-
METHOD_TIMETABLE from hein_analytical_control.constants. You can also provide your own table.
|
185
|
-
|
186
|
-
:param table: the table to create
|
170
|
+
Creates the table for the currently loaded method or sequence.
|
187
171
|
"""
|
188
|
-
self.send(TableOperation.CREATE_TABLE.value.format(register=self.
|
189
|
-
table_name=self.
|
172
|
+
self.send(TableOperation.CREATE_TABLE.value.format(register=self.table_locator.register,
|
173
|
+
table_name=self.table_locator.name))
|
190
174
|
|
191
175
|
def get_num_rows(self) -> Result[Response, str]:
|
192
|
-
self.send(TableOperation.GET_NUM_ROWS.value.format(register=self.
|
193
|
-
table_name=self.
|
176
|
+
self.send(TableOperation.GET_NUM_ROWS.value.format(register=self.table_locator.register,
|
177
|
+
table_name=self.table_locator.name,
|
194
178
|
col_name=RegisterFlag.NUM_ROWS))
|
195
|
-
self.send(Command.GET_ROWS_CMD.value.format(register=self.
|
196
|
-
table_name=self.
|
179
|
+
self.send(Command.GET_ROWS_CMD.value.format(register=self.table_locator.register,
|
180
|
+
table_name=self.table_locator.name,
|
197
181
|
col_name=RegisterFlag.NUM_ROWS))
|
198
182
|
res = self.controller.receive()
|
199
183
|
|
@@ -207,85 +191,95 @@ class TableController(abc.ABC):
|
|
207
191
|
def check_hplc_is_running(self) -> bool:
|
208
192
|
try:
|
209
193
|
started_running = polling.poll(lambda: isinstance(self.controller.get_status(), HPLCRunningStatus),
|
210
|
-
step=
|
194
|
+
step=1, max_tries=20)
|
211
195
|
except Exception as e:
|
212
196
|
print(e)
|
213
197
|
return False
|
198
|
+
if started_running:
|
199
|
+
self.curr_run_starting_time = time.time()
|
214
200
|
return started_running
|
215
201
|
|
216
|
-
def
|
217
|
-
|
218
|
-
|
202
|
+
def check_hplc_run_finished(self) -> Tuple[float, bool]:
|
203
|
+
time_passed = (time.time() - self.curr_run_starting_time)
|
204
|
+
if time_passed > self.timeout:
|
205
|
+
done_running = self.controller.check_if_not_running()
|
206
|
+
enough_time_passed = time_passed >= self.timeout
|
207
|
+
run_finished = enough_time_passed and done_running
|
208
|
+
if run_finished:
|
209
|
+
self._reset_time()
|
210
|
+
return 0, run_finished
|
211
|
+
return (time_passed / self.timeout), self.controller.check_if_not_running()
|
212
|
+
|
213
|
+
def check_hplc_done_running(self) -> Result[Union[SequenceDataFiles, str], str]:
|
219
214
|
"""
|
220
215
|
Checks if ChemStation has finished running and can read data back
|
221
216
|
|
222
|
-
: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
|
223
|
-
:param sequence: if you are running a sequence and want to read back data, the timeout period will be adjusted to be longer than the sequence's runtime
|
224
217
|
:return: Return True if data can be read back, else False.
|
225
218
|
"""
|
226
|
-
timeout = 10 * 60
|
227
|
-
if method:
|
228
|
-
timeout = ((method.stop_time + method.post_time + 3) * 60)
|
229
|
-
if sequence:
|
230
|
-
timeout *= len(sequence.rows)
|
231
|
-
|
232
|
-
most_recent_folder = self.retrieve_recent_data_files()
|
233
|
-
|
234
219
|
finished_run = False
|
220
|
+
minutes = math.ceil(self.timeout / 60)
|
235
221
|
try:
|
236
|
-
finished_run = polling.poll(
|
237
|
-
lambda: self.
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
222
|
+
finished_run = not polling.poll(
|
223
|
+
lambda: self.check_hplc_run_finished()[1],
|
224
|
+
max_tries=minutes - 1, step=50)
|
225
|
+
except (polling.TimeoutException, polling.PollingException, polling.MaxCallException):
|
226
|
+
try:
|
227
|
+
finished_run = polling.poll(
|
228
|
+
lambda: self.check_hplc_run_finished()[1],
|
229
|
+
timeout=self.timeout / 2, step=1)
|
230
|
+
except (polling.TimeoutException, polling.PollingException, polling.MaxCallException):
|
231
|
+
pass
|
232
|
+
|
233
|
+
check_folder = self.fuzzy_match_most_recent_folder(self.data_files[-1])
|
244
234
|
if check_folder.is_ok() and finished_run:
|
245
235
|
return check_folder
|
246
236
|
elif check_folder.is_ok():
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
237
|
+
try:
|
238
|
+
finished_run = polling.poll(
|
239
|
+
lambda: self.check_hplc_run_finished()[1],
|
240
|
+
max_tries=10,
|
241
|
+
step=50)
|
242
|
+
if finished_run:
|
243
|
+
return check_folder
|
244
|
+
except Exception:
|
245
|
+
self._reset_time()
|
252
246
|
return check_folder
|
253
|
-
|
247
|
+
|
254
248
|
else:
|
255
249
|
return Err("Run did not complete as expected")
|
256
250
|
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
potential_folders = []
|
262
|
-
for d in self.data_dirs:
|
263
|
-
subdirs = [x[0] for x in os.walk(d)]
|
264
|
-
potential_folders = sorted(list(filter(lambda d: most_recent_folder in d, subdirs)))
|
265
|
-
if len(potential_folders) > 0:
|
266
|
-
break
|
267
|
-
assert len(potential_folders) > 0
|
268
|
-
parent_dirs = []
|
269
|
-
for folder in potential_folders:
|
270
|
-
path = os.path.normpath(folder)
|
271
|
-
split_folder = path.split(os.sep)
|
272
|
-
if most_recent_folder in split_folder[-1]:
|
273
|
-
parent_dirs.append(folder)
|
274
|
-
parent_dir = sorted(parent_dirs, reverse=True)[0]
|
275
|
-
return Ok(parent_dir)
|
251
|
+
@abc.abstractmethod
|
252
|
+
def fuzzy_match_most_recent_folder(self, most_recent_folder: T) -> Result[T, str]:
|
253
|
+
pass
|
276
254
|
|
277
255
|
@abc.abstractmethod
|
278
|
-
def
|
256
|
+
def get_data(self) -> Union[List[AgilentChannelChromatogramData], AgilentChannelChromatogramData]:
|
279
257
|
pass
|
280
258
|
|
281
259
|
@abc.abstractmethod
|
282
|
-
def
|
260
|
+
def get_report(self, report_type: ReportType = ReportType.TXT) -> List[AgilentReport]:
|
283
261
|
pass
|
284
262
|
|
285
263
|
def get_uv_spectrum(self, path: str):
|
286
264
|
data_uv = rb.agilent.chemstation.parse_file(os.path.join(path, "DAD1.UV"))
|
287
|
-
|
288
|
-
|
265
|
+
times = data_uv.xlabels
|
266
|
+
wavelengthes = data_uv.ylabels
|
267
|
+
data = data_uv.data.transpose()
|
268
|
+
for (i, w) in enumerate(wavelengthes):
|
269
|
+
self.uv[w] = AgilentHPLCChromatogram()
|
270
|
+
self.uv[w].attach_spectrum(times, data[i])
|
271
|
+
|
272
|
+
def get_report_details(self, path: str,
|
273
|
+
report_type: ReportType = ReportType.TXT) -> AgilentReport:
|
274
|
+
if report_type is ReportType.TXT:
|
275
|
+
txt_report = TXTProcessor(path).process_report()
|
276
|
+
if txt_report.is_ok():
|
277
|
+
self.report = txt_report.ok_value
|
278
|
+
if report_type is ReportType.CSV:
|
279
|
+
csv_report = CSVProcessor(path).process_report()
|
280
|
+
if csv_report.is_ok():
|
281
|
+
self.report = csv_report.ok_value
|
282
|
+
return self.report
|
289
283
|
|
290
284
|
def get_spectrum(self, data_path: str, read_uv: bool = False):
|
291
285
|
"""
|
@@ -293,10 +287,13 @@ class TableController(abc.ABC):
|
|
293
287
|
"""
|
294
288
|
if read_uv:
|
295
289
|
self.get_uv_spectrum(data_path)
|
296
|
-
|
297
290
|
for channel, spec in self.spectra.items():
|
298
291
|
try:
|
299
292
|
spec.load_spectrum(data_path=data_path, channel=channel)
|
300
293
|
except FileNotFoundError:
|
301
294
|
self.spectra[channel] = AgilentHPLCChromatogram()
|
302
295
|
print(f"No data at channel: {channel}")
|
296
|
+
|
297
|
+
def _reset_time(self):
|
298
|
+
self.curr_run_starting_time = None
|
299
|
+
self.timeout = None
|
pychemstation/control/hplc.py
CHANGED
@@ -4,15 +4,16 @@ Module to provide API for higher-level HPLC actions.
|
|
4
4
|
Authors: Lucy Hao
|
5
5
|
"""
|
6
6
|
|
7
|
-
from typing import Union, Optional, List
|
7
|
+
from typing import Union, Optional, List, Tuple
|
8
8
|
|
9
9
|
from .controllers.devices.injector import InjectorController
|
10
|
+
from ..analysis.process_report import ReportType, AgilentReport
|
10
11
|
from ..control.controllers import MethodController, SequenceController, CommunicationController
|
11
12
|
from ..utils.chromatogram import AgilentChannelChromatogramData
|
12
13
|
from ..utils.injector_types import InjectorTable
|
13
|
-
from ..utils.macro import Command,
|
14
|
+
from ..utils.macro import Command, Response, Status
|
14
15
|
from ..utils.method_types import MethodDetails
|
15
|
-
from ..utils.sequence_types import SequenceTable
|
16
|
+
from ..utils.sequence_types import SequenceTable
|
16
17
|
from ..utils.table_types import Table
|
17
18
|
|
18
19
|
|
@@ -45,11 +46,12 @@ class HPLCController:
|
|
45
46
|
data_dirs: List[str],
|
46
47
|
offline: bool = False):
|
47
48
|
"""Initialize HPLC controller. The `hplc_talk.mac` macro file must be loaded in the Chemstation software.
|
48
|
-
`comm_dir` must match the file path in the macro file.
|
49
|
+
`comm_dir` must match the file path in the macro file. All file paths are normal strings, with the left slash
|
50
|
+
double escaped: "C:\\my_folder\\"
|
49
51
|
|
50
52
|
:param comm_dir: Name of directory for communication, where ChemStation will read and write from. Can be any existing directory.
|
51
53
|
:param data_dirs: Name of directories for storing data after method or sequence runs. Method data dir is default
|
52
|
-
the first one in the list. In other words, the first dir in the list is highest prio.
|
54
|
+
the first one in the list. In other words, the first dir in the list is highest prio. Must be "normal" strings and not r-strings.
|
53
55
|
:param method_dir: Name of directory where method files are stored.
|
54
56
|
:param sequence_dir: Name of directory where sequence files are stored.
|
55
57
|
:raises FileNotFoundError: If either `data_dir`, `method_dir`, `sequence_dir`, `sequence_data_dir`or `comm_dir` is not a valid directory.
|
@@ -67,22 +69,37 @@ class HPLCController:
|
|
67
69
|
src=sequence_dir,
|
68
70
|
data_dirs=data_dirs,
|
69
71
|
table=self.SEQUENCE_TABLE,
|
70
|
-
|
72
|
+
method_controller=self.method_controller,
|
71
73
|
offline=offline)
|
72
74
|
|
73
75
|
def send(self, cmd: Union[Command, str]):
|
76
|
+
"""
|
77
|
+
Sends any Command or string to Chemstation.
|
78
|
+
|
79
|
+
:param cmd: the macro to send to Chemstation
|
80
|
+
"""
|
74
81
|
if not self.comm:
|
75
82
|
raise RuntimeError(
|
76
83
|
"Communication controller must be initialized before sending command. It is currently in offline mode.")
|
77
84
|
self.comm.send(cmd)
|
78
85
|
|
79
86
|
def receive(self) -> Response:
|
87
|
+
"""
|
88
|
+
Get the most recent response from Chemstation.
|
89
|
+
|
90
|
+
:returns: most recent response from a macro that returned a response.
|
91
|
+
"""
|
80
92
|
if not self.comm:
|
81
93
|
raise RuntimeError(
|
82
94
|
"Communication controller must be initialized before sending command. It is currently in offline mode.")
|
83
95
|
return self.comm.receive().value
|
84
96
|
|
85
|
-
def status(self) ->
|
97
|
+
def status(self) -> Status:
|
98
|
+
"""
|
99
|
+
Get the current status of the HPLC machine.
|
100
|
+
|
101
|
+
:returns: current status of the HPLC machine; Status types can be found in `pychemstation.utils.macro`
|
102
|
+
"""
|
86
103
|
if not self.comm:
|
87
104
|
raise RuntimeError(
|
88
105
|
"Communication controller must be initialized before sending command. It is currently in offline mode.")
|
@@ -103,60 +120,84 @@ class HPLCController:
|
|
103
120
|
def switch_sequence(self, sequence_name: str):
|
104
121
|
"""
|
105
122
|
Allows the user to switch between pre-programmed sequences. The sequence name does not need the '.S' extension.
|
106
|
-
For example
|
123
|
+
For example: for the method named 'mySeq.S', only 'mySeq' is needed.
|
107
124
|
|
108
125
|
:param sequence_name: The name of the sequence file
|
109
126
|
"""
|
110
127
|
self.sequence_controller.switch(sequence_name)
|
111
128
|
|
112
|
-
def run_method(self, experiment_name: str, stall_while_running: bool = True):
|
129
|
+
def run_method(self, experiment_name: str, add_timestamp: bool = True, stall_while_running: bool = True):
|
113
130
|
"""
|
114
131
|
This is the preferred method to trigger a run.
|
115
132
|
Starts the currently selected method, storing data
|
116
133
|
under the <data_dir>/<experiment_name>.D folder.
|
117
|
-
The <experiment_name> will be appended with a timestamp in the '%Y-%m-%d-%H-%M' format.
|
118
134
|
Device must be ready.
|
119
135
|
|
120
136
|
:param experiment_name: Name of the experiment
|
121
|
-
:param stall_while_running: whether
|
137
|
+
:param stall_while_running: whether to return or stall while HPLC runs.
|
138
|
+
:param add_timestamp: whether to append a timestamp in '%Y-%m-%d-%H-%M' format to end of experiment name.
|
122
139
|
"""
|
123
|
-
self.method_controller.run(experiment_name,
|
140
|
+
self.method_controller.run(experiment_name=experiment_name,
|
141
|
+
stall_while_running=stall_while_running,
|
142
|
+
add_timestamp=add_timestamp)
|
143
|
+
|
144
|
+
def stop_method(self):
|
145
|
+
"""Stops the current running method, manual intervention may be needed."""
|
146
|
+
self.method_controller.stop()
|
124
147
|
|
125
148
|
def run_sequence(self, stall_while_running: bool = True):
|
126
149
|
"""
|
127
150
|
Starts the currently loaded sequence, storing data
|
128
|
-
under the
|
151
|
+
under one of the data_dirs/<sequence table name> folder.
|
129
152
|
Device must be ready.
|
130
153
|
|
131
|
-
:param stall_while_running: whether
|
154
|
+
:param stall_while_running: whether to return or stall while HPLC runs.
|
132
155
|
"""
|
133
156
|
self.sequence_controller.run(stall_while_running=stall_while_running)
|
134
157
|
|
158
|
+
def check_method_complete(self) -> Tuple[float, int]:
|
159
|
+
"""
|
160
|
+
Check if the currently running method (if any) is done.
|
161
|
+
|
162
|
+
:returns: the percent of the method run completed, and whether the run is complete.
|
163
|
+
"""
|
164
|
+
return self.method_controller.check_hplc_run_finished()
|
165
|
+
|
166
|
+
def check_sequence_complete(self) -> Tuple[float, int]:
|
167
|
+
"""
|
168
|
+
Check if the currently running sequence (if any) is done.
|
169
|
+
|
170
|
+
:returns: the percent of the sequence run completed, and whether the run is complete.
|
171
|
+
"""
|
172
|
+
return self.sequence_controller.check_hplc_run_finished()
|
173
|
+
|
135
174
|
def edit_method(self, updated_method: MethodDetails, save: bool = False):
|
136
|
-
"""
|
175
|
+
"""
|
176
|
+
Updated the currently loaded method in ChemStation with provided values.
|
137
177
|
|
138
178
|
:param updated_method: the method with updated values, to be sent to Chemstation to modify the currently loaded method.
|
139
|
-
:param save: whether this method should be to disk, or just modified.
|
179
|
+
:param save: whether this method should be saved to disk, or just modified.
|
140
180
|
"""
|
141
181
|
self.method_controller.edit(updated_method, save)
|
142
182
|
|
143
183
|
def edit_sequence(self, updated_sequence: SequenceTable):
|
144
184
|
"""
|
145
|
-
Updates the currently loaded sequence table with the provided table.
|
146
|
-
If you would only like to edit a single row of a sequence table, use `edit_sequence_row` instead.
|
185
|
+
Updates the currently loaded sequence table with the provided table, and saves the sequence.
|
147
186
|
|
148
187
|
:param updated_sequence: The sequence table to be written to the currently loaded sequence table.
|
149
188
|
"""
|
150
189
|
self.sequence_controller.edit(updated_sequence)
|
151
190
|
|
152
|
-
def
|
191
|
+
def get_last_run_method_report(self,
|
192
|
+
custom_path: Optional[str] = None,
|
193
|
+
report_type: ReportType = ReportType.CSV) -> AgilentReport:
|
153
194
|
"""
|
154
|
-
|
155
|
-
|
156
|
-
:param
|
157
|
-
:
|
195
|
+
Return data contained in the REPORT files. Use `aghplctools` if you want more report processing utility.
|
196
|
+
:param custom_path: path to sequence folder
|
197
|
+
:param report_type: read either the TXT or CSV version
|
198
|
+
:returns: report data for method
|
158
199
|
"""
|
159
|
-
self.
|
200
|
+
return self.method_controller.get_report(custom_path=custom_path, report_type=report_type)[0]
|
160
201
|
|
161
202
|
def get_last_run_method_data(self, read_uv: bool = False,
|
162
203
|
data: Optional[str] = None) -> AgilentChannelChromatogramData:
|
@@ -168,8 +209,19 @@ class HPLCController:
|
|
168
209
|
"""
|
169
210
|
return self.method_controller.get_data(custom_path=data, read_uv=read_uv)
|
170
211
|
|
212
|
+
def get_last_run_sequence_reports(self,
|
213
|
+
custom_path: Optional[str] = None,
|
214
|
+
report_type: ReportType = ReportType.CSV) -> List[AgilentReport]:
|
215
|
+
"""
|
216
|
+
Return data contained in the REPORT files. Use `aghplctools` if you want more report processing utility.
|
217
|
+
:param custom_path: path to sequence folder
|
218
|
+
:param report_type: read either the TXT or CSV version
|
219
|
+
:returns: list of reports for each row
|
220
|
+
"""
|
221
|
+
return self.sequence_controller.get_report(custom_path=custom_path, report_type=report_type)
|
222
|
+
|
171
223
|
def get_last_run_sequence_data(self, read_uv: bool = False,
|
172
|
-
data: Optional[str] = None) ->
|
224
|
+
data: Optional[str] = None) -> List[AgilentChannelChromatogramData]:
|
173
225
|
"""
|
174
226
|
Returns data for all rows in the last run sequence data.
|
175
227
|
|
@@ -179,30 +231,23 @@ class HPLCController:
|
|
179
231
|
return self.sequence_controller.get_data(custom_path=data, read_uv=read_uv)
|
180
232
|
|
181
233
|
def check_loaded_sequence(self) -> str:
|
182
|
-
"""
|
183
|
-
Returns the name of the currently loaded sequence.
|
184
|
-
"""
|
234
|
+
"""Returns the name of the currently loaded sequence."""
|
185
235
|
return self.sequence_controller.check()
|
186
236
|
|
187
237
|
def check_loaded_method(self) -> str:
|
188
|
-
"""
|
189
|
-
Returns the name of the currently loaded method.
|
190
|
-
"""
|
238
|
+
"""Returns the name of the currently loaded method."""
|
191
239
|
return self.method_controller.check()
|
192
240
|
|
193
241
|
def load_injector_program(self) -> InjectorTable:
|
242
|
+
"""Returns all details of the injector program for the currently loaded method."""
|
194
243
|
return self.method_controller.injector_controller.load()
|
195
244
|
|
196
245
|
def load_method(self) -> MethodDetails:
|
197
|
-
"""
|
198
|
-
Returns all details of the currently loaded method, including its timetable.
|
199
|
-
"""
|
246
|
+
"""Returns details of the currently loaded method, such as its starting modifier conditions and timetable."""
|
200
247
|
return self.method_controller.load()
|
201
248
|
|
202
249
|
def load_sequence(self) -> SequenceTable:
|
203
|
-
"""
|
204
|
-
Returns the currently loaded sequence.
|
205
|
-
"""
|
250
|
+
"""Returns the currently loaded sequence."""
|
206
251
|
return self.sequence_controller.load()
|
207
252
|
|
208
253
|
def standby(self):
|
@@ -1,5 +1,5 @@
|
|
1
1
|
from dataclasses import dataclass, field
|
2
|
-
from typing import Optional
|
2
|
+
from typing import Optional, List
|
3
3
|
|
4
4
|
|
5
5
|
@dataclass
|
@@ -202,8 +202,8 @@ class StopTime:
|
|
202
202
|
|
203
203
|
@dataclass
|
204
204
|
class Signals:
|
205
|
-
signal:
|
206
|
-
default_factory=
|
205
|
+
signal: List[Signal] = field(
|
206
|
+
default_factory=List,
|
207
207
|
metadata={
|
208
208
|
"name": "Signal",
|
209
209
|
"type": "Element",
|