pychemstation 0.10.3__py3-none-any.whl → 0.10.5__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 +1 -6
- pychemstation/analysis/base_spectrum.py +7 -7
- pychemstation/{utils → analysis}/chromatogram.py +24 -4
- pychemstation/analysis/process_report.py +189 -90
- pychemstation/control/__init__.py +5 -2
- pychemstation/control/controllers/__init__.py +2 -7
- pychemstation/control/controllers/comm.py +56 -32
- pychemstation/control/controllers/devices/device.py +59 -24
- pychemstation/control/controllers/devices/injector.py +33 -10
- pychemstation/control/controllers/tables/__init__.py +4 -0
- pychemstation/control/controllers/tables/method.py +241 -151
- pychemstation/control/controllers/tables/sequence.py +226 -107
- pychemstation/control/controllers/tables/table.py +216 -132
- pychemstation/control/hplc.py +89 -75
- pychemstation/generated/__init__.py +0 -2
- pychemstation/generated/pump_method.py +15 -19
- pychemstation/utils/injector_types.py +1 -1
- pychemstation/utils/macro.py +11 -10
- pychemstation/utils/method_types.py +2 -1
- pychemstation/utils/parsing.py +0 -11
- pychemstation/utils/sequence_types.py +2 -3
- pychemstation/utils/spec_utils.py +2 -3
- pychemstation/utils/table_types.py +10 -9
- pychemstation/utils/tray_types.py +45 -36
- {pychemstation-0.10.3.dist-info → pychemstation-0.10.5.dist-info}/METADATA +5 -4
- pychemstation-0.10.5.dist-info/RECORD +36 -0
- pychemstation/control/controllers/tables/ms.py +0 -21
- pychemstation-0.10.3.dist-info/RECORD +0 -37
- {pychemstation-0.10.3.dist-info → pychemstation-0.10.5.dist-info}/WHEEL +0 -0
- {pychemstation-0.10.3.dist-info → pychemstation-0.10.5.dist-info}/licenses/LICENSE +0 -0
@@ -1,17 +1,20 @@
|
|
1
1
|
import os
|
2
2
|
import time
|
3
|
-
|
3
|
+
import warnings
|
4
|
+
from typing import Dict, List, Optional, Any
|
4
5
|
|
5
6
|
from result import Err, Ok, Result
|
6
7
|
from typing_extensions import override
|
7
8
|
|
8
|
-
from
|
9
|
-
from ....control.controllers.comm import CommunicationController
|
10
|
-
from ....utils.chromatogram import (
|
9
|
+
from pychemstation.analysis.chromatogram import (
|
11
10
|
SEQUENCE_TIME_FORMAT,
|
12
11
|
AgilentChannelChromatogramData,
|
13
12
|
AgilentHPLCChromatogram,
|
14
13
|
)
|
14
|
+
from . import MethodController
|
15
|
+
|
16
|
+
from ....analysis.process_report import AgilentReport, ReportType
|
17
|
+
from ....control.controllers.comm import CommunicationController
|
15
18
|
from ....utils.macro import Command
|
16
19
|
from ....utils.sequence_types import (
|
17
20
|
InjectionSource,
|
@@ -20,9 +23,8 @@ from ....utils.sequence_types import (
|
|
20
23
|
SequenceEntry,
|
21
24
|
SequenceTable,
|
22
25
|
)
|
23
|
-
from ....utils.table_types import RegisterFlag, Table
|
24
|
-
from ....utils.tray_types import FiftyFourVialPlate, TenVialColumn
|
25
|
-
from .. import MethodController
|
26
|
+
from ....utils.table_types import RegisterFlag, T, Table
|
27
|
+
from ....utils.tray_types import FiftyFourVialPlate, TenVialColumn, Tray
|
26
28
|
from .table import TableController
|
27
29
|
|
28
30
|
|
@@ -31,15 +33,24 @@ class SequenceController(TableController):
|
|
31
33
|
Class containing sequence related logic
|
32
34
|
"""
|
33
35
|
|
34
|
-
def __init__(
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
36
|
+
def __init__(
|
37
|
+
self,
|
38
|
+
controller: Optional[CommunicationController],
|
39
|
+
method_controller: MethodController,
|
40
|
+
src: str,
|
41
|
+
data_dirs: List[str],
|
42
|
+
table: Table,
|
43
|
+
offline: bool,
|
44
|
+
):
|
40
45
|
self.method_controller = method_controller
|
41
46
|
self.data_files: List[SequenceDataFiles] = []
|
42
|
-
super().__init__(
|
47
|
+
super().__init__(
|
48
|
+
controller=controller,
|
49
|
+
src=src,
|
50
|
+
data_dirs=data_dirs,
|
51
|
+
table=table,
|
52
|
+
offline=offline,
|
53
|
+
)
|
43
54
|
|
44
55
|
def load(self) -> SequenceTable:
|
45
56
|
rows = self.get_num_rows()
|
@@ -47,29 +58,55 @@ class SequenceController(TableController):
|
|
47
58
|
seq_name = self.receive()
|
48
59
|
|
49
60
|
if rows.is_ok() and seq_name.is_ok():
|
50
|
-
self.table_state = SequenceTable(
|
61
|
+
self.table_state: SequenceTable = SequenceTable(
|
51
62
|
name=seq_name.ok_value.string_response.partition(".S")[0],
|
52
|
-
rows=[
|
63
|
+
rows=[
|
64
|
+
self.get_row(r + 1) for r in range(int(rows.ok_value.num_response))
|
65
|
+
],
|
66
|
+
)
|
53
67
|
return self.table_state
|
54
68
|
raise RuntimeError(rows.err_value)
|
55
69
|
|
70
|
+
def try_int(self, val: Any) -> Optional[int]:
|
71
|
+
try:
|
72
|
+
return int(val)
|
73
|
+
except ValueError:
|
74
|
+
return None
|
75
|
+
|
76
|
+
def try_float(self, val: Any) -> Optional[float]:
|
77
|
+
try:
|
78
|
+
return float(val)
|
79
|
+
except ValueError:
|
80
|
+
return None
|
81
|
+
|
82
|
+
def try_vial_location(self, val: Any) -> Tray:
|
83
|
+
try:
|
84
|
+
return (
|
85
|
+
TenVialColumn(val)
|
86
|
+
if val <= 10
|
87
|
+
else FiftyFourVialPlate.from_int(num=val)
|
88
|
+
)
|
89
|
+
except ValueError:
|
90
|
+
raise ValueError("Expected vial location, is empty.")
|
91
|
+
|
56
92
|
def get_row(self, row: int) -> SequenceEntry:
|
57
93
|
sample_name = self.get_text(row, RegisterFlag.NAME)
|
58
|
-
vial_location =
|
94
|
+
vial_location = self.try_int(self.get_num(row, RegisterFlag.VIAL_LOCATION))
|
59
95
|
method = self.get_text(row, RegisterFlag.METHOD)
|
60
|
-
num_inj =
|
61
|
-
inj_vol =
|
96
|
+
num_inj = self.try_int(self.get_num(row, RegisterFlag.NUM_INJ))
|
97
|
+
inj_vol = self.try_float(self.get_text(row, RegisterFlag.INJ_VOL))
|
62
98
|
inj_source = InjectionSource(self.get_text(row, RegisterFlag.INJ_SOR))
|
63
99
|
sample_type = SampleType(self.get_num(row, RegisterFlag.SAMPLE_TYPE))
|
64
|
-
vial_enum =
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
100
|
+
vial_enum = self.try_vial_location(vial_location)
|
101
|
+
return SequenceEntry(
|
102
|
+
sample_name=sample_name,
|
103
|
+
vial_location=vial_enum,
|
104
|
+
method=None if len(method) == 0 else method,
|
105
|
+
num_inj=num_inj,
|
106
|
+
inj_vol=inj_vol,
|
107
|
+
inj_source=inj_source,
|
108
|
+
sample_type=sample_type,
|
109
|
+
)
|
73
110
|
|
74
111
|
def check(self) -> str:
|
75
112
|
time.sleep(2)
|
@@ -95,7 +132,7 @@ class SequenceController(TableController):
|
|
95
132
|
parsed_response = self.receive().ok_value.string_response
|
96
133
|
|
97
134
|
assert parsed_response == f"{seq_name}.S", "Switching sequence failed."
|
98
|
-
self.table_state =
|
135
|
+
self.table_state = self.load()
|
99
136
|
|
100
137
|
def edit(self, sequence_table: SequenceTable):
|
101
138
|
"""
|
@@ -139,11 +176,14 @@ class SequenceController(TableController):
|
|
139
176
|
num_rows = self.get_num_rows()
|
140
177
|
if row.vial_location:
|
141
178
|
loc = row.vial_location
|
179
|
+
loc_num = -1
|
142
180
|
if isinstance(loc, TenVialColumn):
|
143
|
-
|
181
|
+
loc_num = loc.value
|
144
182
|
elif isinstance(loc, FiftyFourVialPlate):
|
145
|
-
|
146
|
-
self._edit_row_num(
|
183
|
+
loc_num = loc.value()
|
184
|
+
self._edit_row_num(
|
185
|
+
row=row_num, col_name=RegisterFlag.VIAL_LOCATION, val=loc_num
|
186
|
+
)
|
147
187
|
if row.method:
|
148
188
|
method_dir = self.method_controller.src
|
149
189
|
possible_path = os.path.join(method_dir, row.method) + ".M\\"
|
@@ -152,19 +192,35 @@ class SequenceController(TableController):
|
|
152
192
|
method = os.path.join(method_dir, row.method)
|
153
193
|
self._edit_row_text(row=row_num, col_name=RegisterFlag.METHOD, val=method)
|
154
194
|
if row.num_inj:
|
155
|
-
self._edit_row_num(
|
195
|
+
self._edit_row_num(
|
196
|
+
row=row_num, col_name=RegisterFlag.NUM_INJ, val=row.num_inj
|
197
|
+
)
|
156
198
|
if row.inj_vol:
|
157
|
-
self._edit_row_text(
|
199
|
+
self._edit_row_text(
|
200
|
+
row=row_num, col_name=RegisterFlag.INJ_VOL, val=str(row.inj_vol)
|
201
|
+
)
|
158
202
|
if row.inj_source:
|
159
|
-
self._edit_row_text(
|
203
|
+
self._edit_row_text(
|
204
|
+
row=row_num, col_name=RegisterFlag.INJ_SOR, val=row.inj_source.value
|
205
|
+
)
|
160
206
|
if row.sample_name:
|
161
|
-
self._edit_row_text(
|
207
|
+
self._edit_row_text(
|
208
|
+
row=row_num, col_name=RegisterFlag.NAME, val=row.sample_name
|
209
|
+
)
|
162
210
|
if row.data_file:
|
163
|
-
self._edit_row_text(
|
211
|
+
self._edit_row_text(
|
212
|
+
row=row_num, col_name=RegisterFlag.DATA_FILE, val=row.data_file
|
213
|
+
)
|
164
214
|
else:
|
165
|
-
self._edit_row_text(
|
215
|
+
self._edit_row_text(
|
216
|
+
row=row_num, col_name=RegisterFlag.DATA_FILE, val=row.sample_name
|
217
|
+
)
|
166
218
|
if row.sample_type:
|
167
|
-
self._edit_row_num(
|
219
|
+
self._edit_row_num(
|
220
|
+
row=row_num,
|
221
|
+
col_name=RegisterFlag.SAMPLE_TYPE,
|
222
|
+
val=row.sample_type.value,
|
223
|
+
)
|
168
224
|
|
169
225
|
self.send(Command.SAVE_SEQUENCE_CMD)
|
170
226
|
|
@@ -174,23 +230,32 @@ class SequenceController(TableController):
|
|
174
230
|
under the <data_dir>/<sequence table name> folder.
|
175
231
|
Device must be ready.
|
176
232
|
"""
|
177
|
-
self.controller
|
178
|
-
|
233
|
+
if self.controller:
|
234
|
+
self.controller.send(Command.SAVE_METHOD_CMD)
|
235
|
+
self.controller.send(Command.SAVE_SEQUENCE_CMD)
|
236
|
+
else:
|
237
|
+
raise ValueError("Controller is offline!")
|
179
238
|
|
180
239
|
if not self.table_state:
|
181
240
|
self.table_state = self.load()
|
182
241
|
|
183
|
-
total_runtime = 0
|
242
|
+
total_runtime = 0.0
|
184
243
|
for entry in self.table_state.rows:
|
185
244
|
curr_method_runtime = self.method_controller.get_total_runtime()
|
186
245
|
loaded_method = self.method_controller.get_method_name().removesuffix(".M")
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
246
|
+
if entry.method:
|
247
|
+
method_path = entry.method.split(sep="\\")
|
248
|
+
method_name = method_path[-1]
|
249
|
+
if loaded_method != method_name:
|
250
|
+
method_dir = (
|
251
|
+
"\\".join(method_path[:-1]) + "\\"
|
252
|
+
if len(method_path) > 1
|
253
|
+
else None
|
254
|
+
)
|
255
|
+
self.method_controller.switch(
|
256
|
+
method_name=method_name, alt_method_dir=method_dir
|
257
|
+
)
|
258
|
+
curr_method_runtime = self.method_controller.get_total_runtime()
|
194
259
|
total_runtime += curr_method_runtime
|
195
260
|
|
196
261
|
timestamp = time.strftime(SEQUENCE_TIME_FORMAT)
|
@@ -199,7 +264,9 @@ class SequenceController(TableController):
|
|
199
264
|
|
200
265
|
if self.check_hplc_is_running():
|
201
266
|
folder_name = f"{self.table_state.name} {timestamp}"
|
202
|
-
data_file = SequenceDataFiles(
|
267
|
+
data_file = SequenceDataFiles(
|
268
|
+
dir=folder_name, sequence_name=self.table_state.name
|
269
|
+
)
|
203
270
|
self.data_files.append(data_file)
|
204
271
|
|
205
272
|
if stall_while_running:
|
@@ -207,84 +274,136 @@ class SequenceController(TableController):
|
|
207
274
|
if run_completed.is_ok():
|
208
275
|
self.data_files[-1] = run_completed.ok_value
|
209
276
|
else:
|
210
|
-
|
277
|
+
warnings.warn("Run may have not completed.")
|
211
278
|
else:
|
212
279
|
raise RuntimeError("Sequence run did not start.")
|
213
280
|
|
214
281
|
@override
|
215
|
-
def fuzzy_match_most_recent_folder(
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
most_recent_folder.
|
220
|
-
|
221
|
-
|
282
|
+
def fuzzy_match_most_recent_folder(
|
283
|
+
self, most_recent_folder: T
|
284
|
+
) -> Result[SequenceDataFiles, str]:
|
285
|
+
if isinstance(most_recent_folder, SequenceDataFiles):
|
286
|
+
if os.path.isdir(most_recent_folder.dir):
|
287
|
+
subdirs = [x[0] for x in os.walk(most_recent_folder.dir)]
|
288
|
+
potential_folders = sorted(
|
289
|
+
list(filter(lambda d: most_recent_folder.dir in d, subdirs))
|
290
|
+
)
|
291
|
+
most_recent_folder.child_dirs = [
|
292
|
+
f
|
293
|
+
for f in potential_folders
|
294
|
+
if most_recent_folder.dir in f and ".M" not in f and ".D" in f
|
295
|
+
]
|
296
|
+
return Ok(most_recent_folder)
|
222
297
|
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
298
|
+
try:
|
299
|
+
potential_folders = []
|
300
|
+
for d in self.data_dirs:
|
301
|
+
subdirs = [x[0] for x in os.walk(d)]
|
302
|
+
potential_folders = sorted(
|
303
|
+
list(filter(lambda d: most_recent_folder.dir in d, subdirs))
|
304
|
+
)
|
305
|
+
if len(potential_folders) > 0:
|
306
|
+
break
|
307
|
+
assert len(potential_folders) > 0
|
308
|
+
parent_dirs = []
|
309
|
+
for folder in potential_folders:
|
310
|
+
path = os.path.normpath(folder)
|
311
|
+
split_folder = path.split(os.sep)
|
312
|
+
if most_recent_folder.dir in split_folder[-1]:
|
313
|
+
parent_dirs.append(folder)
|
314
|
+
parent_dir = sorted(parent_dirs, reverse=True)[0]
|
315
|
+
|
316
|
+
potential_folders = []
|
317
|
+
for d in self.data_dirs:
|
318
|
+
subdirs = [x[0] for x in os.walk(d)]
|
319
|
+
potential_folders = sorted(
|
320
|
+
list(filter(lambda d: parent_dir in d, subdirs))
|
321
|
+
)
|
322
|
+
if len(potential_folders) > 0:
|
323
|
+
break
|
324
|
+
assert len(potential_folders) > 0
|
325
|
+
most_recent_folder.child_dirs = [
|
326
|
+
f
|
327
|
+
for f in potential_folders
|
328
|
+
if parent_dir in f and ".M" not in f and ".D" in f
|
329
|
+
]
|
330
|
+
return Ok(most_recent_folder)
|
331
|
+
except Exception as e:
|
332
|
+
error = f"Failed to get sequence folder: {e}"
|
333
|
+
return Err(error)
|
334
|
+
return Err("Expected SequenceDataFile type.")
|
335
|
+
|
336
|
+
def get_data_mult_uv(self, custom_path: Optional[str] = None):
|
337
|
+
seq_data_dir = (
|
338
|
+
SequenceDataFiles(dir=custom_path, child_dirs=[], sequence_name="")
|
339
|
+
if custom_path
|
340
|
+
else self.data_files[-1]
|
341
|
+
)
|
342
|
+
if len(seq_data_dir.child_dirs) == 0:
|
343
|
+
self.data_files[-1] = self.fuzzy_match_most_recent_folder(
|
344
|
+
seq_data_dir
|
345
|
+
).ok_value
|
256
346
|
all_w_spectra: List[Dict[int, AgilentHPLCChromatogram]] = []
|
257
347
|
for row in self.data_files[-1].child_dirs:
|
258
|
-
self.
|
259
|
-
all_w_spectra.append(self.uv)
|
348
|
+
all_w_spectra.append(self.get_data_uv(custom_path=row))
|
260
349
|
return all_w_spectra
|
261
350
|
|
262
|
-
def
|
263
|
-
|
264
|
-
|
265
|
-
|
351
|
+
def get_data_uv(
|
352
|
+
self, custom_path: Optional[str] = None
|
353
|
+
) -> Dict[int, AgilentHPLCChromatogram]:
|
354
|
+
if isinstance(custom_path, str):
|
355
|
+
self.get_uv_spectrum(custom_path)
|
356
|
+
return self.uv
|
357
|
+
raise ValueError(
|
358
|
+
"Path should exist when calling from sequence. Provide a child path (contains the method)."
|
359
|
+
)
|
360
|
+
|
361
|
+
def get_data(
|
362
|
+
self, custom_path: Optional[str] = None
|
363
|
+
) -> List[AgilentChannelChromatogramData]:
|
364
|
+
seq_file_dir = (
|
365
|
+
SequenceDataFiles(dir=custom_path, child_dirs=[], sequence_name="")
|
366
|
+
if custom_path
|
367
|
+
else self.data_files[-1]
|
368
|
+
)
|
369
|
+
if len(seq_file_dir.child_dirs) == 0:
|
370
|
+
self.data_files[-1] = self.fuzzy_match_most_recent_folder(
|
371
|
+
seq_file_dir
|
372
|
+
).ok_value
|
266
373
|
spectra: List[AgilentChannelChromatogramData] = []
|
267
374
|
for row in self.data_files[-1].child_dirs:
|
268
375
|
self.get_spectrum_at_channels(row)
|
269
|
-
spectra.append(AgilentChannelChromatogramData(
|
376
|
+
spectra.append(AgilentChannelChromatogramData.from_dict(self.spectra))
|
270
377
|
return spectra
|
271
378
|
|
272
|
-
def get_report(
|
273
|
-
|
379
|
+
def get_report(
|
380
|
+
self,
|
381
|
+
custom_path: Optional[str] = None,
|
382
|
+
report_type: ReportType = ReportType.TXT,
|
383
|
+
) -> List[AgilentReport]:
|
274
384
|
if custom_path:
|
275
385
|
self.data_files.append(
|
276
|
-
self.fuzzy_match_most_recent_folder(
|
277
|
-
|
278
|
-
|
386
|
+
self.fuzzy_match_most_recent_folder(
|
387
|
+
most_recent_folder=SequenceDataFiles(
|
388
|
+
dir=custom_path, child_dirs=[], sequence_name="NA"
|
389
|
+
)
|
390
|
+
).ok_value
|
391
|
+
)
|
279
392
|
parent_dir = self.data_files[-1]
|
280
393
|
spectra = self.get_data()
|
281
394
|
reports = []
|
282
395
|
for i, child_dir in enumerate(parent_dir.child_dirs):
|
283
396
|
metd_report = self.get_report_details(child_dir, report_type)
|
284
|
-
child_spectra: List[AgilentHPLCChromatogram] = list(
|
397
|
+
child_spectra: List[AgilentHPLCChromatogram] = list(
|
398
|
+
spectra[i].__dict__.values()
|
399
|
+
)
|
285
400
|
for j, signal in enumerate(metd_report.signals):
|
286
|
-
|
287
|
-
|
288
|
-
|
401
|
+
assert len(metd_report.signals) <= len(child_spectra)
|
402
|
+
try:
|
403
|
+
possible_data = child_spectra[j]
|
404
|
+
if len(possible_data.x) > 0:
|
405
|
+
signal.data = possible_data
|
406
|
+
except IndexError:
|
407
|
+
raise ValueError(j)
|
289
408
|
reports.append(metd_report)
|
290
409
|
return reports
|