pychemstation 0.10.6__py3-none-any.whl → 0.10.7.dev1__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/analysis/base_spectrum.py +14 -15
  2. pychemstation/analysis/chromatogram.py +7 -8
  3. pychemstation/analysis/process_report.py +7 -15
  4. pychemstation/control/README.md +2 -2
  5. pychemstation/control/controllers/__init__.py +2 -1
  6. pychemstation/control/controllers/comm.py +40 -13
  7. pychemstation/control/controllers/data_aq/method.py +19 -22
  8. pychemstation/control/controllers/data_aq/sequence.py +129 -111
  9. pychemstation/control/controllers/devices/injector.py +7 -7
  10. pychemstation/control/hplc.py +57 -60
  11. pychemstation/utils/__init__.py +23 -0
  12. pychemstation/utils/{mocking → abc_tables}/abc_comm.py +8 -14
  13. pychemstation/utils/abc_tables/device.py +27 -0
  14. pychemstation/{control/controllers → utils}/abc_tables/run.py +69 -34
  15. pychemstation/{control/controllers → utils}/abc_tables/table.py +29 -22
  16. pychemstation/utils/macro.py +13 -0
  17. pychemstation/utils/method_types.py +12 -13
  18. pychemstation/utils/mocking/mock_comm.py +1 -1
  19. pychemstation/utils/num_utils.py +3 -3
  20. pychemstation/utils/sequence_types.py +30 -12
  21. pychemstation/utils/spec_utils.py +42 -66
  22. pychemstation/utils/table_types.py +13 -2
  23. pychemstation/utils/tray_types.py +28 -16
  24. {pychemstation-0.10.6.dist-info → pychemstation-0.10.7.dev1.dist-info}/METADATA +2 -8
  25. pychemstation-0.10.7.dev1.dist-info/RECORD +41 -0
  26. pychemstation/control/controllers/abc_tables/device.py +0 -15
  27. pychemstation/utils/pump_types.py +0 -7
  28. pychemstation-0.10.6.dist-info/RECORD +0 -42
  29. /pychemstation/{control/controllers → utils}/abc_tables/__init__.py +0 -0
  30. {pychemstation-0.10.6.dist-info → pychemstation-0.10.7.dev1.dist-info}/WHEEL +0 -0
  31. {pychemstation-0.10.6.dist-info → pychemstation-0.10.7.dev1.dist-info}/licenses/LICENSE +0 -0
@@ -1,19 +1,19 @@
1
1
  import os
2
2
  import time
3
3
  import warnings
4
- from typing import Any, Dict, List, Optional, Union
4
+ from typing import Any, Dict, List, Optional, Union, Set
5
5
 
6
6
  from result import Err, Ok, Result
7
7
  from typing_extensions import override
8
8
 
9
9
  from pychemstation.analysis.chromatogram import (
10
- SEQUENCE_TIME_FORMAT,
11
10
  AgilentChannelChromatogramData,
12
11
  AgilentHPLCChromatogram,
13
12
  )
14
13
 
15
14
  from ....analysis.process_report import AgilentReport, ReportType
16
15
  from ....control.controllers.comm import CommunicationController
16
+ from ....utils.abc_tables.run import RunController
17
17
  from ....utils.macro import Command
18
18
  from ....utils.sequence_types import (
19
19
  InjectionSource,
@@ -23,8 +23,7 @@ from ....utils.sequence_types import (
23
23
  SequenceTable,
24
24
  )
25
25
  from ....utils.table_types import RegisterFlag, T, Table
26
- from ....utils.tray_types import FiftyFourVialPlate, TenVialColumn, Tray
27
- from ..abc_tables.run import RunController
26
+ from ....utils.tray_types import FiftyFourVialPlate, VialBar, Tray
28
27
  from . import MethodController
29
28
 
30
29
 
@@ -37,8 +36,8 @@ class SequenceController(RunController):
37
36
  self,
38
37
  controller: Optional[CommunicationController],
39
38
  method_controller: MethodController,
40
- src: str,
41
- data_dirs: List[str],
39
+ src: Optional[str],
40
+ data_dirs: Optional[List[str]],
42
41
  table: Table,
43
42
  offline: bool,
44
43
  ):
@@ -81,17 +80,14 @@ class SequenceController(RunController):
81
80
 
82
81
  def try_vial_location(self, val: Any) -> Tray:
83
82
  try:
84
- return (
85
- TenVialColumn(val)
86
- if val <= 10
87
- else FiftyFourVialPlate.from_int(num=val)
88
- )
83
+ return VialBar(val) if val <= 10 else FiftyFourVialPlate.from_int(num=val)
89
84
  except ValueError:
90
85
  raise ValueError("Expected vial location, is empty.")
91
86
 
92
87
  def get_row(self, row: int) -> SequenceEntry:
93
88
  sample_name = self.get_text(row, RegisterFlag.NAME)
94
89
  vial_location = self.try_int(self.get_num(row, RegisterFlag.VIAL_LOCATION))
90
+ data_file = self.get_text(row, RegisterFlag.DATA_FILE)
95
91
  method = self.get_text(row, RegisterFlag.METHOD)
96
92
  num_inj = self.try_int(self.get_num(row, RegisterFlag.NUM_INJ))
97
93
  inj_vol = self.try_float(self.get_text(row, RegisterFlag.INJ_VOL))
@@ -106,17 +102,9 @@ class SequenceController(RunController):
106
102
  inj_vol=inj_vol,
107
103
  inj_source=inj_source,
108
104
  sample_type=sample_type,
105
+ data_file=data_file,
109
106
  )
110
107
 
111
- def check(self) -> str:
112
- time.sleep(2)
113
- self.send(Command.GET_SEQUENCE_CMD)
114
- time.sleep(2)
115
- res = self.receive()
116
- if res.is_ok():
117
- return res.ok_value.string_response
118
- return "ERROR"
119
-
120
108
  def switch(self, seq_name: str):
121
109
  """
122
110
  Switch to the specified sequence. The sequence name does not need the '.S' extension.
@@ -127,13 +115,17 @@ class SequenceController(RunController):
127
115
  self.send(f'_SeqPath$ = "{self.src}"')
128
116
  self.send(Command.SWITCH_SEQUENCE_CMD)
129
117
  time.sleep(2)
130
- self.send(Command.GET_SEQUENCE_CMD)
131
- time.sleep(2)
132
- parsed_response = self.receive().ok_value.string_response
118
+ parsed_response = self.get_current_sequence_name()
133
119
 
134
120
  assert parsed_response == f"{seq_name}.S", "Switching sequence failed."
135
121
  self.table_state = self.load()
136
122
 
123
+ def get_current_sequence_name(self):
124
+ self.send(Command.GET_SEQUENCE_CMD)
125
+ time.sleep(2)
126
+ parsed_response = self.receive().ok_value.string_response
127
+ return parsed_response
128
+
137
129
  def edit(self, sequence_table: SequenceTable):
138
130
  """
139
131
  Updates the currently loaded sequence table with the provided table. This method will delete the existing sequence table and remake it.
@@ -151,13 +143,13 @@ class SequenceController(RunController):
151
143
  self.send(Command.SAVE_SEQUENCE_CMD)
152
144
  for i in range(int(wanted_row_num)):
153
145
  self.add_row()
154
- self.send(Command.SAVE_SEQUENCE_CMD)
146
+ self.save()
155
147
  self.send(Command.SWITCH_SEQUENCE_CMD)
156
148
 
157
149
  for i, row in enumerate(sequence_table.rows):
158
150
  self._edit_row(row=row, row_num=i + 1)
159
151
  self.sleep(1)
160
- self.send(Command.SAVE_SEQUENCE_CMD)
152
+ self.save()
161
153
  self.send(Command.SWITCH_SEQUENCE_CMD)
162
154
 
163
155
  def _edit_row(self, row: SequenceEntry, row_num: int):
@@ -171,70 +163,91 @@ class SequenceController(RunController):
171
163
  if num_rows.is_ok():
172
164
  while num_rows.ok_value.num_response < row_num:
173
165
  self.add_row()
174
- self.send(Command.SAVE_SEQUENCE_CMD)
166
+ self.save()
175
167
  num_rows = self.get_num_rows()
176
168
  if row.vial_location:
177
- self.edit_vial_location(row.vial_location, row_num)
169
+ self.edit_vial_location(row.vial_location, row_num, save=False)
178
170
  if row.method:
179
- self.edit_method_name(row.method, row_num)
171
+ self.edit_method_name(row.method, row_num, save=False)
180
172
  if row.num_inj:
181
- self.edit_num_injections(row.num_inj, row_num)
173
+ self.edit_num_injections(row.num_inj, row_num, save=False)
182
174
  if row.inj_vol:
183
- self.edit_injection_volume(row.inj_vol, row_num)
175
+ self.edit_injection_volume(row.inj_vol, row_num, save=False)
184
176
  if row.inj_source:
185
- self.edit_injection_source(row.inj_source, row_num)
177
+ self.edit_injection_source(row.inj_source, row_num, save=False)
186
178
  if row.sample_name:
187
- self.edit_sample_name(row.sample_name, row_num)
179
+ self.edit_sample_name(row.sample_name, row_num, save=False)
188
180
  if row.data_file:
189
- self.edit_data_file(row.data_file, row_num)
190
- elif row.sample_name and not row.data_file:
191
- self.edit_data_file(row.sample_name, row_num)
181
+ self.edit_data_file(row.data_file, row_num, save=False)
192
182
  if row.sample_type:
193
- self.edit_sample_type(row.sample_type, row_num)
194
- self.send(Command.SAVE_SEQUENCE_CMD)
183
+ self.edit_sample_type(row.sample_type, row_num, save=False)
184
+ self.save()
195
185
 
196
- def edit_sample_type(self, sample_type: SampleType, row_num: int):
186
+ def edit_sample_type(
187
+ self, sample_type: SampleType, row_num: int, save: bool = True
188
+ ):
197
189
  self._edit_row_num(
198
190
  row=row_num,
199
191
  col_name=RegisterFlag.SAMPLE_TYPE,
200
192
  val=sample_type.value,
201
193
  )
194
+ if save:
195
+ self.save()
202
196
 
203
- def edit_data_file(self, data_file: str, row_num: int):
197
+ def edit_data_file(self, data_file: str, row_num: int, save: bool = True):
204
198
  self._edit_row_text(row=row_num, col_name=RegisterFlag.DATA_FILE, val=data_file)
199
+ if save:
200
+ self.save()
205
201
 
206
- def edit_sample_name(self, sample_name: str, row_num: int):
202
+ def edit_sample_name(self, sample_name: str, row_num: int, save: bool = True):
207
203
  self._edit_row_text(row=row_num, col_name=RegisterFlag.NAME, val=sample_name)
204
+ if save:
205
+ self.save()
208
206
 
209
- def edit_injection_source(self, inj_source: InjectionSource, row_num: int):
207
+ def edit_injection_source(
208
+ self, inj_source: InjectionSource, row_num: int, save: bool = True
209
+ ):
210
210
  self._edit_row_text(
211
211
  row=row_num, col_name=RegisterFlag.INJ_SOR, val=inj_source.value
212
212
  )
213
+ if save:
214
+ self.save()
213
215
 
214
- def edit_injection_volume(self, inj_vol: Union[int, float], row_num: int):
216
+ def edit_injection_volume(
217
+ self, inj_vol: Union[int, float], row_num: int, save: bool = True
218
+ ):
215
219
  self._edit_row_text(
216
220
  row=row_num, col_name=RegisterFlag.INJ_VOL, val=str(inj_vol)
217
221
  )
222
+ if save:
223
+ self.save()
218
224
 
219
- def edit_num_injections(self, num_inj: int, row_num: int):
225
+ def edit_num_injections(self, num_inj: int, row_num: int, save: bool = True):
220
226
  self._edit_row_num(row=row_num, col_name=RegisterFlag.NUM_INJ, val=num_inj)
221
227
 
222
- def edit_method_name(self, method: str, row_num: int):
228
+ def edit_method_name(self, method: str, row_num: int, save: bool = True):
223
229
  method_dir = self.method_controller.src
224
230
  possible_path = os.path.join(method_dir, method) + ".M\\"
225
231
  if os.path.exists(possible_path):
226
232
  method = os.path.join(method_dir, method)
227
233
  self._edit_row_text(row=row_num, col_name=RegisterFlag.METHOD, val=method)
234
+ if save:
235
+ self.save()
228
236
 
229
- def edit_vial_location(self, loc: Tray, row_num: int):
237
+ def edit_vial_location(self, loc: Tray, row_num: int, save: bool = True):
230
238
  loc_num = -1
231
- if isinstance(loc, TenVialColumn):
239
+ if isinstance(loc, VialBar):
232
240
  loc_num = loc.value
233
241
  elif isinstance(loc, FiftyFourVialPlate):
234
242
  loc_num = loc.value()
235
243
  self._edit_row_num(
236
244
  row=row_num, col_name=RegisterFlag.VIAL_LOCATION, val=loc_num
237
245
  )
246
+ if save:
247
+ self.save()
248
+
249
+ def save(self):
250
+ self.send(Command.SAVE_SEQUENCE_CMD)
238
251
 
239
252
  def run(self, stall_while_running: bool = True):
240
253
  """
@@ -248,7 +261,8 @@ class SequenceController(RunController):
248
261
  else:
249
262
  raise ValueError("Controller is offline!")
250
263
 
251
- if not self.table_state:
264
+ current_sequence_name = self.get_current_sequence_name()
265
+ if not self.table_state or self.table_state.name not in current_sequence_name:
252
266
  self.table_state = self.load()
253
267
 
254
268
  total_runtime = 0.0
@@ -270,7 +284,6 @@ class SequenceController(RunController):
270
284
  curr_method_runtime = self.method_controller.get_total_runtime()
271
285
  total_runtime += curr_method_runtime
272
286
 
273
- timestamp = time.strftime(SEQUENCE_TIME_FORMAT)
274
287
  self.send(Command.RUN_SEQUENCE_CMD.value)
275
288
  self.timeout = total_runtime * 60
276
289
 
@@ -282,71 +295,70 @@ class SequenceController(RunController):
282
295
  break
283
296
 
284
297
  if hplc_running:
285
- folder_name = f"{self.table_state.name} {timestamp}"
286
- data_file = SequenceDataFiles(
287
- dir=folder_name, sequence_name=self.table_state.name
288
- )
289
- self.data_files.append(data_file)
298
+ full_path_name, current_sample_file = None, None
299
+ for _ in range(5):
300
+ try:
301
+ full_path_name, current_sample_file = (
302
+ self.get_current_run_data_dir_file()
303
+ )
304
+ break
305
+ except ValueError:
306
+ pass
307
+ if full_path_name and current_sample_file:
308
+ data_file = SequenceDataFiles(
309
+ sequence_name=self.table_state.name,
310
+ dir=full_path_name,
311
+ _data_files=[r.data_file for r in self.table_state.rows],
312
+ child_dirs=[os.path.join(full_path_name, current_sample_file)],
313
+ )
314
+ self.data_files.append(data_file)
315
+ else:
316
+ raise ValueError("Data directory for sequence was not found.")
290
317
 
291
318
  if stall_while_running:
292
319
  run_completed = self.check_hplc_done_running()
293
320
  if run_completed.is_ok():
294
321
  self.data_files[-1] = run_completed.ok_value
295
322
  else:
296
- warnings.warn("Run may have not completed.")
323
+ warnings.warn(run_completed.err_value)
297
324
  else:
298
325
  raise RuntimeError("Sequence run may not have started.")
299
326
 
300
327
  @override
301
- def fuzzy_match_most_recent_folder(
302
- self, most_recent_folder: T
328
+ def _fuzzy_match_most_recent_folder(
329
+ self, most_recent_folder: T, child_dirs: Optional[Set[str]]
303
330
  ) -> Result[SequenceDataFiles, str]:
304
331
  if isinstance(most_recent_folder, SequenceDataFiles):
305
- if os.path.isdir(most_recent_folder.dir):
306
- subdirs = [x[0] for x in os.walk(most_recent_folder.dir)]
307
- potential_folders = sorted(
308
- list(filter(lambda d: most_recent_folder.dir in d, subdirs))
309
- )
310
- most_recent_folder.child_dirs = [
311
- f
312
- for f in potential_folders
313
- if most_recent_folder.dir in f and ".M" not in f and ".D" in f
314
- ]
315
- return Ok(most_recent_folder)
316
-
317
332
  try:
318
- potential_folders = []
319
- for d in self.data_dirs:
320
- subdirs = [x[0] for x in os.walk(d)]
321
- potential_folders = sorted(
322
- list(filter(lambda d: most_recent_folder.dir in d, subdirs))
323
- )
324
- if len(potential_folders) > 0:
325
- break
326
- assert len(potential_folders) > 0
327
- parent_dirs = []
328
- for folder in potential_folders:
329
- path = os.path.normpath(folder)
330
- split_folder = path.split(os.sep)
331
- if most_recent_folder.dir in split_folder[-1]:
332
- parent_dirs.append(folder)
333
- parent_dir = sorted(parent_dirs, reverse=True)[0]
334
-
335
- potential_folders = []
336
- for d in self.data_dirs:
337
- subdirs = [x[0] for x in os.walk(d)]
338
- potential_folders = sorted(
339
- list(filter(lambda d: parent_dir in d, subdirs))
340
- )
341
- if len(potential_folders) > 0:
342
- break
343
- assert len(potential_folders) > 0
344
- most_recent_folder.child_dirs = [
345
- f
346
- for f in potential_folders
347
- if parent_dir in f and ".M" not in f and ".D" in f
348
- ]
349
- return Ok(most_recent_folder)
333
+ if most_recent_folder.dir and os.path.isdir(most_recent_folder.dir):
334
+ subdirs = [x[0] for x in os.walk(most_recent_folder.dir)]
335
+ if (
336
+ child_dirs
337
+ and len(child_dirs) > 0
338
+ and set(most_recent_folder._data_files) == child_dirs
339
+ ):
340
+ most_recent_folder.child_dirs = [
341
+ os.path.join(most_recent_folder.dir, f) for f in child_dirs
342
+ ]
343
+ else:
344
+ potential_folders: List[str] = sorted(
345
+ list(
346
+ filter(
347
+ lambda d: most_recent_folder.dir in d,
348
+ subdirs,
349
+ )
350
+ )
351
+ )
352
+ most_recent_folder.child_dirs = [
353
+ f
354
+ for f in potential_folders
355
+ if most_recent_folder.dir in f
356
+ and ".M" not in f
357
+ and ".D" in f
358
+ ]
359
+ return Ok(most_recent_folder)
360
+ else:
361
+ return Err("No sequence folder found, please give the full path.")
350
362
  except Exception as e:
351
363
  error = f"Failed to get sequence folder: {e}"
352
364
  return Err(error)
@@ -354,16 +366,20 @@ class SequenceController(RunController):
354
366
 
355
367
  def get_data_mult_uv(self, custom_path: Optional[str] = None):
356
368
  seq_data_dir = (
357
- SequenceDataFiles(dir=custom_path, child_dirs=[], sequence_name="")
369
+ SequenceDataFiles(dir=custom_path, sequence_name="")
358
370
  if custom_path
359
371
  else self.data_files[-1]
360
372
  )
361
373
  if len(seq_data_dir.child_dirs) == 0:
362
- self.data_files[-1] = self.fuzzy_match_most_recent_folder(
363
- seq_data_dir
364
- ).ok_value
374
+ search_folder = self._fuzzy_match_most_recent_folder(
375
+ seq_data_dir, set(seq_data_dir.child_dirs)
376
+ )
377
+ if search_folder.is_ok():
378
+ seq_data_dir = search_folder.ok_value
379
+ else:
380
+ raise FileNotFoundError(search_folder.err_value)
365
381
  all_w_spectra: List[Dict[int, AgilentHPLCChromatogram]] = []
366
- for row in self.data_files[-1].child_dirs:
382
+ for row in seq_data_dir.child_dirs:
367
383
  all_w_spectra.append(self.get_data_uv(custom_path=row))
368
384
  return all_w_spectra
369
385
 
@@ -381,13 +397,13 @@ class SequenceController(RunController):
381
397
  self, custom_path: Optional[str] = None
382
398
  ) -> List[AgilentChannelChromatogramData]:
383
399
  seq_file_dir = (
384
- SequenceDataFiles(dir=custom_path, child_dirs=[], sequence_name="")
400
+ SequenceDataFiles(dir=custom_path, sequence_name="")
385
401
  if custom_path
386
402
  else self.data_files[-1]
387
403
  )
388
404
  if len(seq_file_dir.child_dirs) == 0:
389
- self.data_files[-1] = self.fuzzy_match_most_recent_folder(
390
- seq_file_dir
405
+ self.data_files[-1] = self._fuzzy_match_most_recent_folder(
406
+ seq_file_dir, set(seq_file_dir.child_dirs)
391
407
  ).ok_value
392
408
  spectra: List[AgilentChannelChromatogramData] = []
393
409
  for row in self.data_files[-1].child_dirs:
@@ -402,10 +418,12 @@ class SequenceController(RunController):
402
418
  ) -> List[AgilentReport]:
403
419
  if custom_path:
404
420
  self.data_files.append(
405
- self.fuzzy_match_most_recent_folder(
421
+ self._fuzzy_match_most_recent_folder(
406
422
  most_recent_folder=SequenceDataFiles(
407
- dir=custom_path, child_dirs=[], sequence_name="NA"
408
- )
423
+ dir=custom_path,
424
+ sequence_name="NA",
425
+ ),
426
+ child_dirs=None,
409
427
  ).ok_value
410
428
  )
411
429
  parent_dir = self.data_files[-1]
@@ -1,5 +1,7 @@
1
- from ..abc_tables.device import DeviceController
1
+ from __future__ import annotations
2
+
2
3
  from ....control.controllers import CommunicationController
4
+ from ....utils.abc_tables.device import DeviceController
3
5
  from ....utils.injector_types import (
4
6
  Draw,
5
7
  Inject,
@@ -15,7 +17,7 @@ from ....utils.injector_types import (
15
17
  )
16
18
  from ....utils.macro import Response
17
19
  from ....utils.table_types import RegisterFlag, Table
18
- from ....utils.tray_types import Tray, FiftyFourVialPlate, TenVialColumn, LocationPlus
20
+ from ....utils.tray_types import Tray, FiftyFourVialPlate, VialBar, LocationPlus
19
21
 
20
22
 
21
23
  class InjectorController(DeviceController):
@@ -29,7 +31,7 @@ class InjectorController(DeviceController):
29
31
  return FiftyFourVialPlate.from_str(val)
30
32
  except Exception:
31
33
  try:
32
- return TenVialColumn(int(val))
34
+ return VialBar(int(val))
33
35
  except Exception:
34
36
  raise ValueError("Location could not be identified.")
35
37
 
@@ -90,7 +92,7 @@ class InjectorController(DeviceController):
90
92
  )
91
93
  raise ValueError("No valid function found.")
92
94
 
93
- def load(self) -> InjectorTable:
95
+ def load(self) -> InjectorTable | None:
94
96
  rows = self.get_num_rows()
95
97
  if rows.is_ok():
96
98
  row_response = rows.value
@@ -101,6 +103,4 @@ class InjectorController(DeviceController):
101
103
  for i in range(int(row_response.num_response))
102
104
  ]
103
105
  )
104
- elif rows.is_err():
105
- return InjectorTable(functions=[])
106
- raise ValueError("Unexpected error")
106
+ raise ValueError("Couldn't read injector table rows.")