pychemstation 0.8.3__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.
Files changed (33) hide show
  1. pychemstation/__init__.py +1 -1
  2. pychemstation/analysis/__init__.py +4 -1
  3. pychemstation/analysis/base_spectrum.py +4 -4
  4. pychemstation/{utils → analysis}/chromatogram.py +4 -7
  5. pychemstation/analysis/process_report.py +121 -74
  6. pychemstation/control/README.md +22 -46
  7. pychemstation/control/__init__.py +5 -0
  8. pychemstation/control/controllers/__init__.py +2 -0
  9. pychemstation/control/controllers/comm.py +39 -18
  10. pychemstation/control/controllers/devices/device.py +27 -14
  11. pychemstation/control/controllers/devices/injector.py +33 -89
  12. pychemstation/control/controllers/tables/method.py +266 -111
  13. pychemstation/control/controllers/tables/ms.py +7 -4
  14. pychemstation/control/controllers/tables/sequence.py +171 -82
  15. pychemstation/control/controllers/tables/table.py +192 -116
  16. pychemstation/control/hplc.py +117 -83
  17. pychemstation/generated/__init__.py +0 -2
  18. pychemstation/generated/dad_method.py +1 -1
  19. pychemstation/generated/pump_method.py +15 -19
  20. pychemstation/utils/injector_types.py +1 -1
  21. pychemstation/utils/macro.py +12 -11
  22. pychemstation/utils/method_types.py +3 -2
  23. pychemstation/{analysis/utils.py → utils/num_utils.py} +2 -2
  24. pychemstation/utils/parsing.py +1 -11
  25. pychemstation/utils/sequence_types.py +4 -5
  26. pychemstation/{analysis → utils}/spec_utils.py +1 -2
  27. pychemstation/utils/table_types.py +10 -9
  28. pychemstation/utils/tray_types.py +48 -38
  29. {pychemstation-0.8.3.dist-info → pychemstation-0.9.0.dist-info}/METADATA +63 -24
  30. pychemstation-0.9.0.dist-info/RECORD +37 -0
  31. pychemstation-0.8.3.dist-info/RECORD +0 -37
  32. {pychemstation-0.8.3.dist-info → pychemstation-0.9.0.dist-info}/WHEEL +0 -0
  33. {pychemstation-0.8.3.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
- from typing import Union, Optional, List, Tuple, Dict
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, Err
16
-
17
- from ....analysis.process_report import AgilentReport, ReportType, CSVProcessor, TXTProcessor
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 ....utils.chromatogram import AgilentHPLCChromatogram, AgilentChannelChromatogramData
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 SequenceDataFiles, SequenceTable
23
- from ....utils.table_types import Table, TableOperation, RegisterFlag, T
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
- def __init__(self, controller: CommunicationController,
31
- src: Optional[str],
32
- data_dirs: Optional[List[str]],
33
- table: Table,
34
- offline: bool = False):
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[str, AgilentHPLCChromatogram] = {}
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(TableOperation.GET_ROW_VAL.value.format(register=self.table_locator.register,
94
- table_name=self.table_locator.name,
95
- row=row,
96
- col_name=col_name.value))
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(register=self.table_locator.register,
101
- table_name=self.table_locator.name,
102
- row=row,
103
- col_name=col_name.value))
104
-
105
- def add_new_col_num(self,
106
- col_name: RegisterFlag,
107
- val: Union[int, float]):
108
- self.sleepy_send(TableOperation.NEW_COL_VAL.value.format(
109
- register=self.table_locator.register,
110
- table_name=self.table_locator.name,
111
- col_name=col_name,
112
- val=val))
113
-
114
- def add_new_col_text(self,
115
- col_name: RegisterFlag,
116
- val: str):
117
- self.sleepy_send(TableOperation.NEW_COL_TEXT.value.format(
118
- register=self.table_locator.register,
119
- table_name=self.table_locator.name,
120
- col_name=col_name,
121
- val=val))
122
-
123
- def _edit_row_num(self,
124
- col_name: RegisterFlag,
125
- val: Union[int, float],
126
- row: Optional[int] = None):
127
- self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(
128
- register=self.table_locator.register,
129
- table_name=self.table_locator.name,
130
- row=row if row is not None else 'Rows',
131
- col_name=col_name,
132
- val=val))
133
-
134
- def _edit_row_text(self,
135
- col_name: RegisterFlag,
136
- val: str,
137
- row: Optional[int] = None):
138
- self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(
139
- register=self.table_locator.register,
140
- table_name=self.table_locator.name,
141
- row=row if row is not None else 'Rows',
142
- col_name=col_name,
143
- val=val))
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(TableOperation.DELETE_ROW.value.format(register=self.table_locator.register,
151
- table_name=self.table_locator.name,
152
- row=row))
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(TableOperation.NEW_ROW.value.format(register=self.table_locator.register,
159
- table_name=self.table_locator.name))
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(TableOperation.DELETE_TABLE.value.format(register=self.table_locator.register,
166
- table_name=self.table_locator.name))
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(TableOperation.CREATE_TABLE.value.format(register=self.table_locator.register,
173
- table_name=self.table_locator.name))
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(TableOperation.GET_NUM_ROWS.value.format(register=self.table_locator.register,
177
- table_name=self.table_locator.name,
178
- col_name=RegisterFlag.NUM_ROWS))
179
- self.send(Command.GET_ROWS_CMD.value.format(register=self.table_locator.register,
180
- table_name=self.table_locator.name,
181
- col_name=RegisterFlag.NUM_ROWS))
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('Print Rows')
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(lambda: isinstance(self.controller.get_status(), HPLCRunningStatus),
194
- step=1, max_tries=20)
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
- 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]:
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: Return True if data can be read back, else False.
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, step=50)
225
- except (polling.TimeoutException, polling.PollingException, polling.MaxCallException):
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, step=1)
230
- except (polling.TimeoutException, polling.PollingException, polling.MaxCallException):
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
- max_tries=10,
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 check_folder
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(self) -> Union[List[AgilentChannelChromatogramData], AgilentChannelChromatogramData]:
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(self, report_type: ReportType = ReportType.TXT) -> List[AgilentReport]:
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
- wavelengthes = data_uv.ylabels
267
- data = data_uv.data.transpose()
268
- for (i, w) in enumerate(wavelengthes):
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, data[i])
346
+ self.uv[w].attach_spectrum(times, absorbances[i])
271
347
 
272
- def get_report_details(self, path: str,
273
- report_type: ReportType = ReportType.TXT) -> AgilentReport:
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 get_spectrum(self, data_path: str, read_uv: bool = False):
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
- print(f"No data at channel: {channel}")
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