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.
Files changed (31) hide show
  1. pychemstation/__init__.py +1 -1
  2. pychemstation/analysis/__init__.py +1 -6
  3. pychemstation/analysis/base_spectrum.py +7 -7
  4. pychemstation/{utils → analysis}/chromatogram.py +24 -4
  5. pychemstation/analysis/process_report.py +189 -90
  6. pychemstation/control/__init__.py +5 -2
  7. pychemstation/control/controllers/__init__.py +2 -7
  8. pychemstation/control/controllers/comm.py +56 -32
  9. pychemstation/control/controllers/devices/device.py +59 -24
  10. pychemstation/control/controllers/devices/injector.py +33 -10
  11. pychemstation/control/controllers/tables/__init__.py +4 -0
  12. pychemstation/control/controllers/tables/method.py +241 -151
  13. pychemstation/control/controllers/tables/sequence.py +226 -107
  14. pychemstation/control/controllers/tables/table.py +216 -132
  15. pychemstation/control/hplc.py +89 -75
  16. pychemstation/generated/__init__.py +0 -2
  17. pychemstation/generated/pump_method.py +15 -19
  18. pychemstation/utils/injector_types.py +1 -1
  19. pychemstation/utils/macro.py +11 -10
  20. pychemstation/utils/method_types.py +2 -1
  21. pychemstation/utils/parsing.py +0 -11
  22. pychemstation/utils/sequence_types.py +2 -3
  23. pychemstation/utils/spec_utils.py +2 -3
  24. pychemstation/utils/table_types.py +10 -9
  25. pychemstation/utils/tray_types.py +45 -36
  26. {pychemstation-0.10.3.dist-info → pychemstation-0.10.5.dist-info}/METADATA +5 -4
  27. pychemstation-0.10.5.dist-info/RECORD +36 -0
  28. pychemstation/control/controllers/tables/ms.py +0 -21
  29. pychemstation-0.10.3.dist-info/RECORD +0 -37
  30. {pychemstation-0.10.3.dist-info → pychemstation-0.10.5.dist-info}/WHEEL +0 -0
  31. {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
- from typing import Dict, List, Optional
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 ....analysis.process_report import AgilentReport, ReportType
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__(self, controller: CommunicationController,
35
- method_controller: MethodController,
36
- src: str,
37
- data_dirs: List[str],
38
- table: Table,
39
- offline: bool):
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__(controller=controller, src=src, data_dirs=data_dirs, table=table, offline=offline)
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=[self.get_row(r + 1) for r in range(int(rows.ok_value.num_response))])
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 = int(self.get_num(row, RegisterFlag.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 = int(self.get_num(row, RegisterFlag.NUM_INJ))
61
- inj_vol = float(self.get_text(row, RegisterFlag.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 = TenVialColumn(vial_location) if vial_location <= 10 else FiftyFourVialPlate.from_int(
65
- num=vial_location)
66
- return SequenceEntry(sample_name=sample_name,
67
- vial_location=vial_enum,
68
- method=None if len(method) == 0 else method,
69
- num_inj=num_inj,
70
- inj_vol=inj_vol,
71
- inj_source=inj_source,
72
- sample_type=sample_type)
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 = None
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
- loc = row.vial_location.value
181
+ loc_num = loc.value
144
182
  elif isinstance(loc, FiftyFourVialPlate):
145
- loc = row.vial_location.value()
146
- self._edit_row_num(row=row_num, col_name=RegisterFlag.VIAL_LOCATION, val=loc)
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(row=row_num, col_name=RegisterFlag.NUM_INJ, val=row.num_inj)
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(row=row_num, col_name=RegisterFlag.INJ_VOL, val=row.inj_vol)
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(row=row_num, col_name=RegisterFlag.INJ_SOR, val=row.inj_source.value)
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(row=row_num, col_name=RegisterFlag.NAME, val=row.sample_name)
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(row=row_num, col_name=RegisterFlag.DATA_FILE, val=row.data_file)
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(row=row_num, col_name=RegisterFlag.DATA_FILE, val=row.sample_name)
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(row=row_num, col_name=RegisterFlag.SAMPLE_TYPE, val=row.sample_type.value)
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.send(Command.SAVE_METHOD_CMD)
178
- self.controller.send(Command.SAVE_SEQUENCE_CMD)
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
- method_path = entry.method.split(sep="\\")
188
- method_name = method_path[-1]
189
- if loaded_method != method_name:
190
- method_dir = "\\".join(method_path[:-1])+"\\" if len(method_path) > 1 else None
191
- self.method_controller.switch(method_name=method_name,
192
- alt_method_dir=method_dir)
193
- curr_method_runtime = self.method_controller.get_total_runtime()
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(dir=folder_name, sequence_name=self.table_state.name)
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
- raise RuntimeError("Run error has occurred.")
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(self, most_recent_folder: SequenceDataFiles) -> Result[SequenceDataFiles, str]:
216
- if os.path.isdir(most_recent_folder.dir):
217
- subdirs = [x[0] for x in os.walk(most_recent_folder.dir)]
218
- potential_folders = sorted(list(filter(lambda d: most_recent_folder.dir in d, subdirs)))
219
- most_recent_folder.child_dirs = [f for f in potential_folders if
220
- most_recent_folder.dir in f and ".M" not in f and ".D" in f]
221
- return Ok(most_recent_folder)
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
- try:
224
- potential_folders = []
225
- for d in self.data_dirs:
226
- subdirs = [x[0] for x in os.walk(d)]
227
- potential_folders = sorted(list(filter(lambda d: most_recent_folder.dir in d, subdirs)))
228
- if len(potential_folders) > 0:
229
- break
230
- assert len(potential_folders) > 0
231
- parent_dirs = []
232
- for folder in potential_folders:
233
- path = os.path.normpath(folder)
234
- split_folder = path.split(os.sep)
235
- if most_recent_folder.dir in split_folder[-1]:
236
- parent_dirs.append(folder)
237
- parent_dir = sorted(parent_dirs, reverse=True)[0]
238
-
239
- potential_folders = []
240
- for d in self.data_dirs:
241
- subdirs = [x[0] for x in os.walk(d)]
242
- potential_folders = sorted(list(filter(lambda d: parent_dir in d, subdirs)))
243
- if len(potential_folders) > 0:
244
- break
245
- assert len(potential_folders) > 0
246
- most_recent_folder.child_dirs = [f for f in potential_folders if
247
- parent_dir in f and ".M" not in f and ".D" in f]
248
- return Ok(most_recent_folder)
249
- except Exception:
250
- return Err("Failed to get sequence folder")
251
-
252
- def get_data_uv(self,custom_path: Optional[str] = None) -> List[Dict[int, AgilentHPLCChromatogram]]:
253
- custom_path = SequenceDataFiles(dir=custom_path, child_dirs=[], sequence_name="") if custom_path else self.data_files[-1]
254
- if len(custom_path.child_dirs) == 0:
255
- self.data_files[-1] = self.fuzzy_match_most_recent_folder(custom_path).ok_value
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.get_uv_spectrum(row)
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 get_data(self, custom_path: Optional[str] = None) -> List[AgilentChannelChromatogramData]:
263
- custom_path = SequenceDataFiles(dir=custom_path, child_dirs=[], sequence_name="") if custom_path else self.data_files[-1]
264
- if len(custom_path.child_dirs) == 0:
265
- self.data_files[-1] = self.fuzzy_match_most_recent_folder(custom_path).ok_value
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(**self.spectra))
376
+ spectra.append(AgilentChannelChromatogramData.from_dict(self.spectra))
270
377
  return spectra
271
378
 
272
- def get_report(self, custom_path: Optional[str] = None,
273
- report_type: ReportType = ReportType.TXT) -> List[AgilentReport]:
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(most_recent_folder=SequenceDataFiles(dir=custom_path,
277
- child_dirs=[],
278
- sequence_name="NA")).ok_value)
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(spectra[i].__dict__.values())
397
+ child_spectra: List[AgilentHPLCChromatogram] = list(
398
+ spectra[i].__dict__.values()
399
+ )
285
400
  for j, signal in enumerate(metd_report.signals):
286
- possible_data = child_spectra[j]
287
- if len(possible_data.x) > 0:
288
- signal.data = possible_data
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