pychemstation 0.8.4__py3-none-any.whl → 0.9.0__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/__init__.py +1 -1
- pychemstation/analysis/__init__.py +4 -1
- pychemstation/analysis/base_spectrum.py +4 -4
- pychemstation/{utils → analysis}/chromatogram.py +4 -7
- pychemstation/analysis/process_report.py +118 -71
- pychemstation/control/README.md +22 -46
- pychemstation/control/__init__.py +5 -0
- pychemstation/control/controllers/__init__.py +2 -0
- pychemstation/control/controllers/comm.py +39 -18
- pychemstation/control/controllers/devices/device.py +27 -14
- pychemstation/control/controllers/devices/injector.py +33 -89
- pychemstation/control/controllers/tables/method.py +266 -111
- pychemstation/control/controllers/tables/ms.py +7 -4
- pychemstation/control/controllers/tables/sequence.py +171 -82
- pychemstation/control/controllers/tables/table.py +192 -116
- pychemstation/control/hplc.py +117 -83
- pychemstation/generated/__init__.py +0 -2
- pychemstation/generated/dad_method.py +1 -1
- pychemstation/generated/pump_method.py +15 -19
- pychemstation/utils/injector_types.py +1 -1
- pychemstation/utils/macro.py +12 -11
- pychemstation/utils/method_types.py +3 -2
- pychemstation/{analysis/utils.py → utils/num_utils.py} +2 -2
- pychemstation/utils/parsing.py +1 -11
- pychemstation/utils/sequence_types.py +4 -5
- pychemstation/{analysis → utils}/spec_utils.py +1 -2
- pychemstation/utils/table_types.py +10 -9
- pychemstation/utils/tray_types.py +48 -38
- {pychemstation-0.8.4.dist-info → pychemstation-0.9.0.dist-info}/METADATA +63 -24
- pychemstation-0.9.0.dist-info/RECORD +37 -0
- pychemstation-0.8.4.dist-info/RECORD +0 -37
- {pychemstation-0.8.4.dist-info → pychemstation-0.9.0.dist-info}/WHEEL +0 -0
- {pychemstation-0.8.4.dist-info → pychemstation-0.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -4,34 +4,47 @@ Abstract module containing shared logic for Method and Sequence tables.
|
|
4
4
|
Authors: Lucy Hao
|
5
5
|
"""
|
6
6
|
|
7
|
+
from __future__ import annotations
|
8
|
+
|
7
9
|
import abc
|
8
10
|
import math
|
9
11
|
import os
|
10
12
|
import time
|
11
|
-
|
13
|
+
import warnings
|
14
|
+
from typing import Dict, List, Optional, Tuple, Union
|
12
15
|
|
13
16
|
import polling
|
14
17
|
import rainbow as rb
|
15
|
-
from result import Result,
|
16
|
-
|
17
|
-
from ....analysis.process_report import
|
18
|
+
from result import Err, Result, Ok
|
19
|
+
|
20
|
+
from ....analysis.process_report import (
|
21
|
+
AgilentReport,
|
22
|
+
CSVProcessor,
|
23
|
+
ReportType,
|
24
|
+
TXTProcessor,
|
25
|
+
)
|
18
26
|
from ....control.controllers.comm import CommunicationController
|
19
|
-
from
|
27
|
+
from pychemstation.analysis.chromatogram import (
|
28
|
+
AgilentChannelChromatogramData,
|
29
|
+
AgilentHPLCChromatogram,
|
30
|
+
)
|
20
31
|
from ....utils.macro import Command, HPLCRunningStatus, Response
|
21
32
|
from ....utils.method_types import MethodDetails
|
22
|
-
from ....utils.sequence_types import
|
23
|
-
from ....utils.table_types import
|
33
|
+
from ....utils.sequence_types import SequenceTable
|
34
|
+
from ....utils.table_types import RegisterFlag, T, Table, TableOperation
|
24
35
|
|
25
36
|
TableType = Union[MethodDetails, SequenceTable]
|
26
37
|
|
27
38
|
|
28
39
|
class TableController(abc.ABC):
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
40
|
+
def __init__(
|
41
|
+
self,
|
42
|
+
controller: CommunicationController,
|
43
|
+
src: Optional[str],
|
44
|
+
data_dirs: Optional[List[str]],
|
45
|
+
table: Table,
|
46
|
+
offline: bool = False,
|
47
|
+
):
|
35
48
|
self.controller = controller
|
36
49
|
self.table_locator = table
|
37
50
|
self.table_state: Optional[TableType] = None
|
@@ -61,7 +74,7 @@ class TableController(abc.ABC):
|
|
61
74
|
"H": AgilentHPLCChromatogram(),
|
62
75
|
}
|
63
76
|
self.report: Optional[AgilentReport] = None
|
64
|
-
self.uv: Dict[
|
77
|
+
self.uv: Dict[int, AgilentHPLCChromatogram] = {}
|
65
78
|
self.data_files: List = []
|
66
79
|
|
67
80
|
def receive(self) -> Result[Response, str]:
|
@@ -75,7 +88,8 @@ class TableController(abc.ABC):
|
|
75
88
|
def send(self, cmd: Union[Command, str]):
|
76
89
|
if not self.controller:
|
77
90
|
raise RuntimeError(
|
78
|
-
"Communication controller must be initialized before sending command. It is currently in offline mode."
|
91
|
+
"Communication controller must be initialized before sending command. It is currently in offline mode."
|
92
|
+
)
|
79
93
|
self.controller.send(cmd)
|
80
94
|
|
81
95
|
def sleepy_send(self, cmd: Union[Command, str]):
|
@@ -90,108 +104,145 @@ class TableController(abc.ABC):
|
|
90
104
|
self.send(Command.SLEEP_CMD.value.format(seconds=seconds))
|
91
105
|
|
92
106
|
def get_num(self, row: int, col_name: RegisterFlag) -> Union[int, float]:
|
93
|
-
return self.controller.get_num_val(
|
94
|
-
|
95
|
-
|
96
|
-
|
107
|
+
return self.controller.get_num_val(
|
108
|
+
TableOperation.GET_ROW_VAL.value.format(
|
109
|
+
register=self.table_locator.register,
|
110
|
+
table_name=self.table_locator.name,
|
111
|
+
row=row,
|
112
|
+
col_name=col_name.value,
|
113
|
+
)
|
114
|
+
)
|
97
115
|
|
98
116
|
def get_text(self, row: int, col_name: RegisterFlag) -> str:
|
99
117
|
return self.controller.get_text_val(
|
100
|
-
TableOperation.GET_ROW_TEXT.value.format(
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
118
|
+
TableOperation.GET_ROW_TEXT.value.format(
|
119
|
+
register=self.table_locator.register,
|
120
|
+
table_name=self.table_locator.name,
|
121
|
+
row=row,
|
122
|
+
col_name=col_name.value,
|
123
|
+
)
|
124
|
+
)
|
125
|
+
|
126
|
+
def add_new_col_num(self, col_name: RegisterFlag, val: Union[int, float]):
|
127
|
+
self.sleepy_send(
|
128
|
+
TableOperation.NEW_COL_VAL.value.format(
|
129
|
+
register=self.table_locator.register,
|
130
|
+
table_name=self.table_locator.name,
|
131
|
+
col_name=col_name,
|
132
|
+
val=val,
|
133
|
+
)
|
134
|
+
)
|
135
|
+
|
136
|
+
def add_new_col_text(self, col_name: RegisterFlag, val: str):
|
137
|
+
self.sleepy_send(
|
138
|
+
TableOperation.NEW_COL_TEXT.value.format(
|
139
|
+
register=self.table_locator.register,
|
140
|
+
table_name=self.table_locator.name,
|
141
|
+
col_name=col_name,
|
142
|
+
val=val,
|
143
|
+
)
|
144
|
+
)
|
145
|
+
|
146
|
+
def _edit_row_num(
|
147
|
+
self, col_name: RegisterFlag, val: Union[int, float], row: Optional[int] = None
|
148
|
+
):
|
149
|
+
self.sleepy_send(
|
150
|
+
TableOperation.EDIT_ROW_VAL.value.format(
|
151
|
+
register=self.table_locator.register,
|
152
|
+
table_name=self.table_locator.name,
|
153
|
+
row=row if row is not None else "Rows",
|
154
|
+
col_name=col_name,
|
155
|
+
val=val,
|
156
|
+
)
|
157
|
+
)
|
158
|
+
|
159
|
+
def _edit_row_text(
|
160
|
+
self, col_name: RegisterFlag, val: str, row: Optional[int] = None
|
161
|
+
):
|
162
|
+
self.sleepy_send(
|
163
|
+
TableOperation.EDIT_ROW_TEXT.value.format(
|
164
|
+
register=self.table_locator.register,
|
165
|
+
table_name=self.table_locator.name,
|
166
|
+
row=row if row is not None else "Rows",
|
167
|
+
col_name=col_name,
|
168
|
+
val=val,
|
169
|
+
)
|
170
|
+
)
|
144
171
|
|
145
172
|
@abc.abstractmethod
|
146
173
|
def get_row(self, row: int):
|
147
174
|
pass
|
148
175
|
|
149
176
|
def delete_row(self, row: int):
|
150
|
-
self.sleepy_send(
|
151
|
-
|
152
|
-
|
177
|
+
self.sleepy_send(
|
178
|
+
TableOperation.DELETE_ROW.value.format(
|
179
|
+
register=self.table_locator.register,
|
180
|
+
table_name=self.table_locator.name,
|
181
|
+
row=row,
|
182
|
+
)
|
183
|
+
)
|
153
184
|
|
154
185
|
def add_row(self):
|
155
186
|
"""
|
156
187
|
Adds a row to the provided table for currently loaded method or sequence.
|
157
188
|
"""
|
158
|
-
self.sleepy_send(
|
159
|
-
|
189
|
+
self.sleepy_send(
|
190
|
+
TableOperation.NEW_ROW.value.format(
|
191
|
+
register=self.table_locator.register, table_name=self.table_locator.name
|
192
|
+
)
|
193
|
+
)
|
160
194
|
|
161
195
|
def delete_table(self):
|
162
196
|
"""
|
163
197
|
Deletes the table for the current loaded method or sequence.
|
164
198
|
"""
|
165
|
-
self.sleepy_send(
|
166
|
-
|
199
|
+
self.sleepy_send(
|
200
|
+
TableOperation.DELETE_TABLE.value.format(
|
201
|
+
register=self.table_locator.register, table_name=self.table_locator.name
|
202
|
+
)
|
203
|
+
)
|
167
204
|
|
168
205
|
def new_table(self):
|
169
206
|
"""
|
170
207
|
Creates the table for the currently loaded method or sequence.
|
171
208
|
"""
|
172
|
-
self.send(
|
173
|
-
|
209
|
+
self.send(
|
210
|
+
TableOperation.CREATE_TABLE.value.format(
|
211
|
+
register=self.table_locator.register, table_name=self.table_locator.name
|
212
|
+
)
|
213
|
+
)
|
174
214
|
|
175
215
|
def get_num_rows(self) -> Result[Response, str]:
|
176
|
-
self.send(
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
216
|
+
self.send(
|
217
|
+
TableOperation.GET_NUM_ROWS.value.format(
|
218
|
+
register=self.table_locator.register,
|
219
|
+
table_name=self.table_locator.name,
|
220
|
+
col_name=RegisterFlag.NUM_ROWS,
|
221
|
+
)
|
222
|
+
)
|
223
|
+
self.send(
|
224
|
+
Command.GET_ROWS_CMD.value.format(
|
225
|
+
register=self.table_locator.register,
|
226
|
+
table_name=self.table_locator.name,
|
227
|
+
col_name=RegisterFlag.NUM_ROWS,
|
228
|
+
)
|
229
|
+
)
|
182
230
|
res = self.controller.receive()
|
183
231
|
|
184
232
|
if res.is_ok():
|
185
233
|
self.send("Sleep 0.1")
|
186
|
-
self.send(
|
234
|
+
self.send("Print Rows")
|
187
235
|
return res
|
188
236
|
else:
|
189
237
|
return Err("No rows could be read.")
|
190
238
|
|
191
239
|
def check_hplc_is_running(self) -> bool:
|
192
240
|
try:
|
193
|
-
started_running = polling.poll(
|
194
|
-
|
241
|
+
started_running = polling.poll(
|
242
|
+
lambda: isinstance(self.controller.get_status(), HPLCRunningStatus),
|
243
|
+
step=1,
|
244
|
+
max_tries=20,
|
245
|
+
)
|
195
246
|
except Exception as e:
|
196
247
|
print(e)
|
197
248
|
return False
|
@@ -200,34 +251,50 @@ class TableController(abc.ABC):
|
|
200
251
|
return started_running
|
201
252
|
|
202
253
|
def check_hplc_run_finished(self) -> Tuple[float, bool]:
|
203
|
-
|
204
|
-
if
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
254
|
+
done_running = self.controller.check_if_not_running()
|
255
|
+
if self.curr_run_starting_time and self.timeout:
|
256
|
+
time_passed = time.time() - self.curr_run_starting_time
|
257
|
+
if time_passed > self.timeout:
|
258
|
+
enough_time_passed = time_passed >= self.timeout
|
259
|
+
run_finished = enough_time_passed and done_running
|
260
|
+
if run_finished:
|
261
|
+
self._reset_time()
|
262
|
+
return 0, run_finished
|
263
|
+
else:
|
264
|
+
time_left = self.timeout - time_passed
|
265
|
+
return time_left, self.controller.check_if_not_running()
|
266
|
+
return 0, self.controller.check_if_not_running()
|
267
|
+
|
268
|
+
def check_hplc_done_running(self) -> Ok[T] | Err[str]:
|
214
269
|
"""
|
215
270
|
Checks if ChemStation has finished running and can read data back
|
216
271
|
|
217
|
-
:return:
|
272
|
+
:return: Data file object containing most recent run file information.
|
218
273
|
"""
|
219
274
|
finished_run = False
|
220
275
|
minutes = math.ceil(self.timeout / 60)
|
221
276
|
try:
|
222
277
|
finished_run = not polling.poll(
|
223
278
|
lambda: self.check_hplc_run_finished()[1],
|
224
|
-
max_tries=minutes - 1,
|
225
|
-
|
279
|
+
max_tries=minutes - 1,
|
280
|
+
step=50,
|
281
|
+
)
|
282
|
+
except (
|
283
|
+
polling.TimeoutException,
|
284
|
+
polling.PollingException,
|
285
|
+
polling.MaxCallException,
|
286
|
+
):
|
226
287
|
try:
|
227
288
|
finished_run = polling.poll(
|
228
289
|
lambda: self.check_hplc_run_finished()[1],
|
229
|
-
timeout=self.timeout / 2,
|
230
|
-
|
290
|
+
timeout=self.timeout / 2,
|
291
|
+
step=1,
|
292
|
+
)
|
293
|
+
except (
|
294
|
+
polling.TimeoutException,
|
295
|
+
polling.PollingException,
|
296
|
+
polling.MaxCallException,
|
297
|
+
):
|
231
298
|
pass
|
232
299
|
|
233
300
|
check_folder = self.fuzzy_match_most_recent_folder(self.data_files[-1])
|
@@ -236,41 +303,51 @@ class TableController(abc.ABC):
|
|
236
303
|
elif check_folder.is_ok():
|
237
304
|
try:
|
238
305
|
finished_run = polling.poll(
|
239
|
-
lambda: self.check_hplc_run_finished()[1],
|
240
|
-
|
241
|
-
step=50)
|
306
|
+
lambda: self.check_hplc_run_finished()[1], max_tries=10, step=50
|
307
|
+
)
|
242
308
|
if finished_run:
|
243
309
|
return check_folder
|
244
310
|
except Exception:
|
245
311
|
self._reset_time()
|
246
|
-
return
|
247
|
-
|
248
|
-
else:
|
249
|
-
return Err("Run did not complete as expected")
|
312
|
+
return self.data_files[-1]
|
313
|
+
return Err("Run did not complete as expected")
|
250
314
|
|
251
315
|
@abc.abstractmethod
|
252
316
|
def fuzzy_match_most_recent_folder(self, most_recent_folder: T) -> Result[T, str]:
|
253
317
|
pass
|
254
318
|
|
255
319
|
@abc.abstractmethod
|
256
|
-
def get_data(
|
320
|
+
def get_data(
|
321
|
+
self, custom_path: Optional[str] = None
|
322
|
+
) -> Union[List[AgilentChannelChromatogramData], AgilentChannelChromatogramData]:
|
323
|
+
pass
|
324
|
+
|
325
|
+
@abc.abstractmethod
|
326
|
+
def get_data_uv(
|
327
|
+
self,
|
328
|
+
) -> Union[
|
329
|
+
List[Dict[str, AgilentHPLCChromatogram]], Dict[str, AgilentHPLCChromatogram]
|
330
|
+
]:
|
257
331
|
pass
|
258
332
|
|
259
333
|
@abc.abstractmethod
|
260
|
-
def get_report(
|
334
|
+
def get_report(
|
335
|
+
self, report_type: ReportType = ReportType.TXT
|
336
|
+
) -> List[AgilentReport]:
|
261
337
|
pass
|
262
338
|
|
263
339
|
def get_uv_spectrum(self, path: str):
|
264
340
|
data_uv = rb.agilent.chemstation.parse_file(os.path.join(path, "DAD1.UV"))
|
265
341
|
times = data_uv.xlabels
|
266
|
-
|
267
|
-
|
268
|
-
for
|
342
|
+
wavelengths = data_uv.ylabels
|
343
|
+
absorbances = data_uv.data.transpose()
|
344
|
+
for i, w in enumerate(wavelengths):
|
269
345
|
self.uv[w] = AgilentHPLCChromatogram()
|
270
|
-
self.uv[w].attach_spectrum(times,
|
346
|
+
self.uv[w].attach_spectrum(times, absorbances[i])
|
271
347
|
|
272
|
-
def get_report_details(
|
273
|
-
|
348
|
+
def get_report_details(
|
349
|
+
self, path: str, report_type: ReportType = ReportType.TXT
|
350
|
+
) -> AgilentReport:
|
274
351
|
if report_type is ReportType.TXT:
|
275
352
|
txt_report = TXTProcessor(path).process_report()
|
276
353
|
if txt_report.is_ok():
|
@@ -281,18 +358,17 @@ class TableController(abc.ABC):
|
|
281
358
|
self.report = csv_report.ok_value
|
282
359
|
return self.report
|
283
360
|
|
284
|
-
def
|
361
|
+
def get_spectrum_at_channels(self, data_path: str):
|
285
362
|
"""
|
286
363
|
Load chromatogram for any channel in spectra dictionary.
|
287
364
|
"""
|
288
|
-
if read_uv:
|
289
|
-
self.get_uv_spectrum(data_path)
|
290
365
|
for channel, spec in self.spectra.items():
|
291
366
|
try:
|
292
367
|
spec.load_spectrum(data_path=data_path, channel=channel)
|
293
368
|
except FileNotFoundError:
|
294
369
|
self.spectra[channel] = AgilentHPLCChromatogram()
|
295
|
-
|
370
|
+
warning = f"No data at channel: {channel}"
|
371
|
+
warnings.warn(warning)
|
296
372
|
|
297
373
|
def _reset_time(self):
|
298
374
|
self.curr_run_starting_time = None
|