pychemstation 0.7.0.dev1__py3-none-any.whl → 0.8.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 (49) hide show
  1. pychemstation/analysis/base_spectrum.py +3 -6
  2. pychemstation/analysis/process_report.py +248 -225
  3. pychemstation/analysis/utils.py +3 -1
  4. pychemstation/control/README.md +124 -0
  5. pychemstation/control/controllers/README.md +1 -0
  6. pychemstation/control/controllers/__init__.py +0 -2
  7. pychemstation/control/controllers/comm.py +27 -20
  8. pychemstation/control/controllers/devices/device.py +17 -4
  9. pychemstation/control/controllers/tables/method.py +57 -39
  10. pychemstation/control/controllers/tables/sequence.py +98 -31
  11. pychemstation/control/controllers/tables/table.py +121 -126
  12. pychemstation/control/hplc.py +82 -37
  13. pychemstation/generated/dad_method.py +3 -3
  14. pychemstation/generated/pump_method.py +7 -7
  15. pychemstation/out.txt +145 -0
  16. pychemstation/tests.ipynb +310 -0
  17. pychemstation/utils/chromatogram.py +5 -1
  18. pychemstation/utils/injector_types.py +2 -2
  19. pychemstation/utils/macro.py +1 -1
  20. pychemstation/utils/table_types.py +3 -0
  21. pychemstation/utils/tray_types.py +59 -39
  22. {pychemstation-0.7.0.dev1.dist-info → pychemstation-0.8.0.dist-info}/METADATA +25 -21
  23. pychemstation-0.8.0.dist-info/RECORD +39 -0
  24. {pychemstation-0.7.0.dev1.dist-info → pychemstation-0.8.0.dist-info}/WHEEL +1 -2
  25. pychemstation/control/comm.py +0 -206
  26. pychemstation/control/controllers/devices/column.py +0 -12
  27. pychemstation/control/controllers/devices/dad.py +0 -0
  28. pychemstation/control/controllers/devices/pump.py +0 -43
  29. pychemstation/control/controllers/method.py +0 -338
  30. pychemstation/control/controllers/sequence.py +0 -190
  31. pychemstation/control/controllers/table_controller.py +0 -266
  32. pychemstation/control/table/__init__.py +0 -3
  33. pychemstation/control/table/method.py +0 -274
  34. pychemstation/control/table/sequence.py +0 -210
  35. pychemstation/control/table/table_controller.py +0 -201
  36. pychemstation-0.7.0.dev1.dist-info/RECORD +0 -58
  37. pychemstation-0.7.0.dev1.dist-info/top_level.txt +0 -2
  38. tests/__init__.py +0 -0
  39. tests/constants.py +0 -88
  40. tests/test_comb.py +0 -136
  41. tests/test_comm.py +0 -65
  42. tests/test_inj.py +0 -39
  43. tests/test_method.py +0 -99
  44. tests/test_nightly.py +0 -80
  45. tests/test_proc_rep.py +0 -52
  46. tests/test_runs_stable.py +0 -125
  47. tests/test_sequence.py +0 -125
  48. tests/test_stable.py +0 -283
  49. {pychemstation-0.7.0.dev1.dist-info → pychemstation-0.8.0.dist-info/licenses}/LICENSE +0 -0
@@ -5,32 +5,26 @@ Authors: Lucy Hao
5
5
  """
6
6
 
7
7
  import abc
8
+ import math
8
9
  import os
9
- import warnings
10
- from dataclasses import dataclass
11
- from typing import Union, Optional, AnyStr, List
10
+ import time
11
+ from typing import Union, Optional, List, Tuple, Dict
12
12
 
13
- import numpy as np
14
13
  import polling
15
14
  import rainbow as rb
16
- from result import Result, Ok, Err
15
+ from result import Result, Err
17
16
 
17
+ from ....analysis.process_report import AgilentReport, ReportType, CSVProcessor, TXTProcessor
18
18
  from ....control.controllers.comm import CommunicationController
19
19
  from ....utils.chromatogram import AgilentHPLCChromatogram, AgilentChannelChromatogramData
20
20
  from ....utils.macro import Command, HPLCRunningStatus, Response
21
21
  from ....utils.method_types import MethodDetails
22
22
  from ....utils.sequence_types import SequenceDataFiles, SequenceTable
23
- from ....utils.table_types import Table, TableOperation, RegisterFlag
23
+ from ....utils.table_types import Table, TableOperation, RegisterFlag, T
24
24
 
25
25
  TableType = Union[MethodDetails, SequenceTable]
26
26
 
27
27
 
28
- @dataclass
29
- class ChromData:
30
- x: np.array
31
- y: np.array
32
-
33
-
34
28
  class TableController(abc.ABC):
35
29
 
36
30
  def __init__(self, controller: CommunicationController,
@@ -39,36 +33,36 @@ class TableController(abc.ABC):
39
33
  table: Table,
40
34
  offline: bool = False):
41
35
  self.controller = controller
42
- self.table = table
36
+ self.table_locator = table
43
37
  self.table_state: Optional[TableType] = None
38
+ self.curr_run_starting_time: Optional[float] = None
39
+ self.timeout: Optional[float] = None
44
40
 
45
41
  if not offline:
46
- # Initialize row counter for table operations
47
- self.send('Local Rows')
48
-
49
- if src and os.path.isdir(src):
50
- self.src: str = src
51
- elif isinstance(src, str):
42
+ if src and not os.path.isdir(src):
52
43
  raise FileNotFoundError(f"dir: {src} not found.")
53
44
 
54
- if data_dirs:
55
- for d in data_dirs:
56
- if not os.path.isdir(d):
57
- raise FileNotFoundError(f"dir: {d} not found.")
58
- self.data_dirs: List[str] = data_dirs
59
-
60
- self.spectra: dict[str, Optional[AgilentHPLCChromatogram]] = {
61
- "A": AgilentHPLCChromatogram(),
62
- "B": AgilentHPLCChromatogram(),
63
- "C": AgilentHPLCChromatogram(),
64
- "D": AgilentHPLCChromatogram(),
65
- "E": AgilentHPLCChromatogram(),
66
- "F": AgilentHPLCChromatogram(),
67
- "G": AgilentHPLCChromatogram(),
68
- "H": AgilentHPLCChromatogram(),
69
- }
70
- self.data_files: Union[list[SequenceDataFiles], list[str]] = []
71
- self.uv = None
45
+ for d in data_dirs:
46
+ if not os.path.isdir(d):
47
+ raise FileNotFoundError(f"dir: {d} not found.")
48
+ if r"\\" in d:
49
+ raise ValueError("Data directories should not be raw strings!")
50
+ self.src: str = src
51
+ self.data_dirs: List[str] = data_dirs
52
+
53
+ self.spectra: dict[str, Optional[AgilentHPLCChromatogram]] = {
54
+ "A": AgilentHPLCChromatogram(),
55
+ "B": AgilentHPLCChromatogram(),
56
+ "C": AgilentHPLCChromatogram(),
57
+ "D": AgilentHPLCChromatogram(),
58
+ "E": AgilentHPLCChromatogram(),
59
+ "F": AgilentHPLCChromatogram(),
60
+ "G": AgilentHPLCChromatogram(),
61
+ "H": AgilentHPLCChromatogram(),
62
+ }
63
+ self.report: Optional[AgilentReport] = None
64
+ self.uv: Dict[str, AgilentHPLCChromatogram] = {}
65
+ self.data_files: List = []
72
66
 
73
67
  def receive(self) -> Result[Response, str]:
74
68
  for _ in range(10):
@@ -96,23 +90,24 @@ class TableController(abc.ABC):
96
90
  self.send(Command.SLEEP_CMD.value.format(seconds=seconds))
97
91
 
98
92
  def get_num(self, row: int, col_name: RegisterFlag) -> Union[int, float]:
99
- return self.controller.get_num_val(TableOperation.GET_ROW_VAL.value.format(register=self.table.register,
100
- table_name=self.table.name,
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,
101
95
  row=row,
102
96
  col_name=col_name.value))
103
97
 
104
98
  def get_text(self, row: int, col_name: RegisterFlag) -> str:
105
- return self.controller.get_text_val(TableOperation.GET_ROW_TEXT.value.format(register=self.table.register,
106
- table_name=self.table.name,
107
- row=row,
108
- col_name=col_name.value))
99
+ 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))
109
104
 
110
105
  def add_new_col_num(self,
111
106
  col_name: RegisterFlag,
112
107
  val: Union[int, float]):
113
108
  self.sleepy_send(TableOperation.NEW_COL_VAL.value.format(
114
- register=self.table.register,
115
- table_name=self.table.name,
109
+ register=self.table_locator.register,
110
+ table_name=self.table_locator.name,
116
111
  col_name=col_name,
117
112
  val=val))
118
113
 
@@ -120,8 +115,8 @@ class TableController(abc.ABC):
120
115
  col_name: RegisterFlag,
121
116
  val: str):
122
117
  self.sleepy_send(TableOperation.NEW_COL_TEXT.value.format(
123
- register=self.table.register,
124
- table_name=self.table.name,
118
+ register=self.table_locator.register,
119
+ table_name=self.table_locator.name,
125
120
  col_name=col_name,
126
121
  val=val))
127
122
 
@@ -130,8 +125,8 @@ class TableController(abc.ABC):
130
125
  val: Union[int, float],
131
126
  row: Optional[int] = None):
132
127
  self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(
133
- register=self.table.register,
134
- table_name=self.table.name,
128
+ register=self.table_locator.register,
129
+ table_name=self.table_locator.name,
135
130
  row=row if row is not None else 'Rows',
136
131
  col_name=col_name,
137
132
  val=val))
@@ -141,8 +136,8 @@ class TableController(abc.ABC):
141
136
  val: str,
142
137
  row: Optional[int] = None):
143
138
  self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(
144
- register=self.table.register,
145
- table_name=self.table.name,
139
+ register=self.table_locator.register,
140
+ table_name=self.table_locator.name,
146
141
  row=row if row is not None else 'Rows',
147
142
  col_name=col_name,
148
143
  val=val))
@@ -152,48 +147,37 @@ class TableController(abc.ABC):
152
147
  pass
153
148
 
154
149
  def delete_row(self, row: int):
155
- self.sleepy_send(TableOperation.DELETE_ROW.value.format(register=self.table.register,
156
- table_name=self.table.name,
150
+ self.sleepy_send(TableOperation.DELETE_ROW.value.format(register=self.table_locator.register,
151
+ table_name=self.table_locator.name,
157
152
  row=row))
158
153
 
159
154
  def add_row(self):
160
155
  """
161
156
  Adds a row to the provided table for currently loaded method or sequence.
162
- Import either the SEQUENCE_TABLE or METHOD_TIMETABLE from hein_analytical_control.constants.
163
- You can also provide your own table.
164
-
165
- :param table: the table to add a new row to
166
157
  """
167
- self.sleepy_send(TableOperation.NEW_ROW.value.format(register=self.table.register,
168
- table_name=self.table.name))
158
+ self.sleepy_send(TableOperation.NEW_ROW.value.format(register=self.table_locator.register,
159
+ table_name=self.table_locator.name))
169
160
 
170
161
  def delete_table(self):
171
162
  """
172
163
  Deletes the table for the current loaded method or sequence.
173
- Import either the SEQUENCE_TABLE or METHOD_TIMETABLE from hein_analytical_control.constants.
174
- You can also provide your own table.
175
-
176
- :param table: the table to delete
177
164
  """
178
- self.sleepy_send(TableOperation.DELETE_TABLE.value.format(register=self.table.register,
179
- table_name=self.table.name))
165
+ self.sleepy_send(TableOperation.DELETE_TABLE.value.format(register=self.table_locator.register,
166
+ table_name=self.table_locator.name))
180
167
 
181
168
  def new_table(self):
182
169
  """
183
- Creates the table for the currently loaded method or sequence. Import either the SEQUENCE_TABLE or
184
- METHOD_TIMETABLE from hein_analytical_control.constants. You can also provide your own table.
185
-
186
- :param table: the table to create
170
+ Creates the table for the currently loaded method or sequence.
187
171
  """
188
- self.send(TableOperation.CREATE_TABLE.value.format(register=self.table.register,
189
- table_name=self.table.name))
172
+ self.send(TableOperation.CREATE_TABLE.value.format(register=self.table_locator.register,
173
+ table_name=self.table_locator.name))
190
174
 
191
175
  def get_num_rows(self) -> Result[Response, str]:
192
- self.send(TableOperation.GET_NUM_ROWS.value.format(register=self.table.register,
193
- table_name=self.table.name,
176
+ self.send(TableOperation.GET_NUM_ROWS.value.format(register=self.table_locator.register,
177
+ table_name=self.table_locator.name,
194
178
  col_name=RegisterFlag.NUM_ROWS))
195
- self.send(Command.GET_ROWS_CMD.value.format(register=self.table.register,
196
- table_name=self.table.name,
179
+ self.send(Command.GET_ROWS_CMD.value.format(register=self.table_locator.register,
180
+ table_name=self.table_locator.name,
197
181
  col_name=RegisterFlag.NUM_ROWS))
198
182
  res = self.controller.receive()
199
183
 
@@ -207,85 +191,93 @@ class TableController(abc.ABC):
207
191
  def check_hplc_is_running(self) -> bool:
208
192
  try:
209
193
  started_running = polling.poll(lambda: isinstance(self.controller.get_status(), HPLCRunningStatus),
210
- step=3, max_tries=10)
194
+ step=1, max_tries=20)
211
195
  except Exception as e:
212
196
  print(e)
213
197
  return False
198
+ if started_running:
199
+ self.curr_run_starting_time = time.time()
214
200
  return started_running
215
201
 
216
- def check_hplc_done_running(self,
217
- method: Optional[MethodDetails] = None,
218
- sequence: Optional[SequenceTable] = None) -> Result[str, str]:
202
+ 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]:
219
214
  """
220
215
  Checks if ChemStation has finished running and can read data back
221
216
 
222
- :param method: if you are running a method and want to read back data, the timeout period will be adjusted to be longer than the method's runtime
223
- :param sequence: if you are running a sequence and want to read back data, the timeout period will be adjusted to be longer than the sequence's runtime
224
217
  :return: Return True if data can be read back, else False.
225
218
  """
226
- timeout = 10 * 60
227
- if method:
228
- timeout = ((method.stop_time + method.post_time + 3) * 60)
229
- if sequence:
230
- timeout *= len(sequence.rows)
231
-
232
- most_recent_folder = self.retrieve_recent_data_files()
233
-
234
219
  finished_run = False
220
+ minutes = math.ceil(self.timeout / 60)
235
221
  try:
236
- finished_run = polling.poll(
237
- lambda: self.controller.check_if_running(),
238
- timeout=timeout,
239
- step=50)
240
- except Exception:
241
- pass
242
-
243
- check_folder = self.fuzzy_match_most_recent_folder(most_recent_folder)
222
+ finished_run = not polling.poll(
223
+ lambda: self.check_hplc_run_finished()[1],
224
+ max_tries=minutes - 1, step=50)
225
+ except (polling.TimeoutException, polling.PollingException, polling.MaxCallException):
226
+ try:
227
+ finished_run = polling.poll(
228
+ lambda: self.check_hplc_run_finished()[1],
229
+ timeout=self.timeout / 2, step=1)
230
+ except (polling.TimeoutException, polling.PollingException, polling.MaxCallException):
231
+ pass
232
+
233
+ check_folder = self.fuzzy_match_most_recent_folder(self.data_files[-1])
244
234
  if check_folder.is_ok() and finished_run:
245
235
  return check_folder
246
236
  elif check_folder.is_ok():
247
- finished_run = polling.poll(
248
- lambda: self.controller.check_if_running(),
249
- timeout=timeout,
250
- step=50)
251
- if finished_run:
237
+ try:
238
+ finished_run = polling.poll(
239
+ lambda: self.check_hplc_run_finished()[1],
240
+ max_tries=10,
241
+ step=50)
242
+ if finished_run:
243
+ return check_folder
244
+ except Exception:
245
+ self._reset_time()
252
246
  return check_folder
253
- return check_folder
247
+
254
248
  else:
255
249
  return Err("Run did not complete as expected")
256
250
 
257
- def fuzzy_match_most_recent_folder(self, most_recent_folder) -> Result[str, str]:
258
- if os.path.exists(most_recent_folder):
259
- return Ok(most_recent_folder)
260
-
261
- subdirs = []
262
- for d in self.data_dirs:
263
- subdirs = [x[0] for x in os.walk(d)]
264
- if len(subdirs) > 0:
265
- break
266
- assert len(subdirs) > 0
267
- potential_folders = sorted(list(filter(lambda d: most_recent_folder in d, subdirs)))
268
- parent_dirs = []
269
- for folder in potential_folders:
270
- path = os.path.normpath(folder)
271
- split_folder = path.split(os.sep)
272
- if most_recent_folder in split_folder[-1]:
273
- parent_dirs.append(folder)
274
- parent_dir = sorted(parent_dirs, reverse=True)[0]
275
- return Ok(parent_dir)
251
+ @abc.abstractmethod
252
+ def fuzzy_match_most_recent_folder(self, most_recent_folder: T) -> Result[T, str]:
253
+ pass
276
254
 
277
255
  @abc.abstractmethod
278
- def retrieve_recent_data_files(self):
256
+ def get_data(self) -> Union[List[AgilentChannelChromatogramData], AgilentChannelChromatogramData]:
279
257
  pass
280
258
 
281
259
  @abc.abstractmethod
282
- def get_data(self) -> Union[list[AgilentChannelChromatogramData], AgilentChannelChromatogramData]:
260
+ def get_report(self, report_type: ReportType = ReportType.TXT) -> List[AgilentReport]:
283
261
  pass
284
262
 
285
263
  def get_uv_spectrum(self, path: str):
286
264
  data_uv = rb.agilent.chemstation.parse_file(os.path.join(path, "DAD1.UV"))
287
265
  zipped_data = zip(data_uv.ylabels, data_uv.data)
288
- self.uv = {str(w_a[0]): ChromData(x=data_uv.xlabels, y=w_a[1]) for w_a in zipped_data}
266
+ for (wavelength, data) in zipped_data:
267
+ self.uv[wavelength] = AgilentHPLCChromatogram()
268
+ self.uv[wavelength].attach_spectrum(data_uv.xlabels, data)
269
+
270
+ def get_report_details(self, path: str,
271
+ report_type: ReportType = ReportType.TXT) -> AgilentReport:
272
+ if report_type is ReportType.TXT:
273
+ txt_report = TXTProcessor(path).process_report()
274
+ if txt_report.is_ok():
275
+ self.report = txt_report.ok_value
276
+ if report_type is ReportType.CSV:
277
+ csv_report = CSVProcessor(path).process_report()
278
+ if csv_report.is_ok():
279
+ self.report = csv_report.ok_value
280
+ return self.report
289
281
 
290
282
  def get_spectrum(self, data_path: str, read_uv: bool = False):
291
283
  """
@@ -293,10 +285,13 @@ class TableController(abc.ABC):
293
285
  """
294
286
  if read_uv:
295
287
  self.get_uv_spectrum(data_path)
296
-
297
288
  for channel, spec in self.spectra.items():
298
289
  try:
299
290
  spec.load_spectrum(data_path=data_path, channel=channel)
300
291
  except FileNotFoundError:
301
292
  self.spectra[channel] = AgilentHPLCChromatogram()
302
293
  print(f"No data at channel: {channel}")
294
+
295
+ def _reset_time(self):
296
+ self.curr_run_starting_time = None
297
+ self.timeout = None
@@ -4,15 +4,16 @@ Module to provide API for higher-level HPLC actions.
4
4
  Authors: Lucy Hao
5
5
  """
6
6
 
7
- from typing import Union, Optional, List
7
+ from typing import Union, Optional, List, Tuple
8
8
 
9
9
  from .controllers.devices.injector import InjectorController
10
+ from ..analysis.process_report import ReportType, AgilentReport
10
11
  from ..control.controllers import MethodController, SequenceController, CommunicationController
11
12
  from ..utils.chromatogram import AgilentChannelChromatogramData
12
13
  from ..utils.injector_types import InjectorTable
13
- from ..utils.macro import Command, HPLCRunningStatus, HPLCAvailStatus, HPLCErrorStatus, Response
14
+ from ..utils.macro import Command, Response, Status
14
15
  from ..utils.method_types import MethodDetails
15
- from ..utils.sequence_types import SequenceTable, SequenceEntry
16
+ from ..utils.sequence_types import SequenceTable
16
17
  from ..utils.table_types import Table
17
18
 
18
19
 
@@ -45,11 +46,12 @@ class HPLCController:
45
46
  data_dirs: List[str],
46
47
  offline: bool = False):
47
48
  """Initialize HPLC controller. The `hplc_talk.mac` macro file must be loaded in the Chemstation software.
48
- `comm_dir` must match the file path in the macro file.
49
+ `comm_dir` must match the file path in the macro file. All file paths are normal strings, with the left slash
50
+ double escaped: "C:\\my_folder\\"
49
51
 
50
52
  :param comm_dir: Name of directory for communication, where ChemStation will read and write from. Can be any existing directory.
51
53
  :param data_dirs: Name of directories for storing data after method or sequence runs. Method data dir is default
52
- the first one in the list. In other words, the first dir in the list is highest prio.
54
+ the first one in the list. In other words, the first dir in the list is highest prio. Must be "normal" strings and not r-strings.
53
55
  :param method_dir: Name of directory where method files are stored.
54
56
  :param sequence_dir: Name of directory where sequence files are stored.
55
57
  :raises FileNotFoundError: If either `data_dir`, `method_dir`, `sequence_dir`, `sequence_data_dir`or `comm_dir` is not a valid directory.
@@ -67,22 +69,37 @@ class HPLCController:
67
69
  src=sequence_dir,
68
70
  data_dirs=data_dirs,
69
71
  table=self.SEQUENCE_TABLE,
70
- method_dir=method_dir,
72
+ method_controller=self.method_controller,
71
73
  offline=offline)
72
74
 
73
75
  def send(self, cmd: Union[Command, str]):
76
+ """
77
+ Sends any Command or string to Chemstation.
78
+
79
+ :param cmd: the macro to send to Chemstation
80
+ """
74
81
  if not self.comm:
75
82
  raise RuntimeError(
76
83
  "Communication controller must be initialized before sending command. It is currently in offline mode.")
77
84
  self.comm.send(cmd)
78
85
 
79
86
  def receive(self) -> Response:
87
+ """
88
+ Get the most recent response from Chemstation.
89
+
90
+ :returns: most recent response from a macro that returned a response.
91
+ """
80
92
  if not self.comm:
81
93
  raise RuntimeError(
82
94
  "Communication controller must be initialized before sending command. It is currently in offline mode.")
83
95
  return self.comm.receive().value
84
96
 
85
- def status(self) -> Union[HPLCRunningStatus | HPLCAvailStatus | HPLCErrorStatus]:
97
+ def status(self) -> Status:
98
+ """
99
+ Get the current status of the HPLC machine.
100
+
101
+ :returns: current status of the HPLC machine; Status types can be found in `pychemstation.utils.macro`
102
+ """
86
103
  if not self.comm:
87
104
  raise RuntimeError(
88
105
  "Communication controller must be initialized before sending command. It is currently in offline mode.")
@@ -103,60 +120,84 @@ class HPLCController:
103
120
  def switch_sequence(self, sequence_name: str):
104
121
  """
105
122
  Allows the user to switch between pre-programmed sequences. The sequence name does not need the '.S' extension.
106
- For example. for the method named 'mySeq.S', only 'mySeq' is needed.
123
+ For example: for the method named 'mySeq.S', only 'mySeq' is needed.
107
124
 
108
125
  :param sequence_name: The name of the sequence file
109
126
  """
110
127
  self.sequence_controller.switch(sequence_name)
111
128
 
112
- def run_method(self, experiment_name: str, stall_while_running: bool = True):
129
+ def run_method(self, experiment_name: str, add_timestamp: bool = True, stall_while_running: bool = True):
113
130
  """
114
131
  This is the preferred method to trigger a run.
115
132
  Starts the currently selected method, storing data
116
133
  under the <data_dir>/<experiment_name>.D folder.
117
- The <experiment_name> will be appended with a timestamp in the '%Y-%m-%d-%H-%M' format.
118
134
  Device must be ready.
119
135
 
120
136
  :param experiment_name: Name of the experiment
121
- :param stall_while_running: whether this method should return or stall while HPLC runs.
137
+ :param stall_while_running: whether to return or stall while HPLC runs.
138
+ :param add_timestamp: whether to append a timestamp in '%Y-%m-%d-%H-%M' format to end of experiment name.
122
139
  """
123
- self.method_controller.run(experiment_name, stall_while_running)
140
+ self.method_controller.run(experiment_name=experiment_name,
141
+ stall_while_running=stall_while_running,
142
+ add_timestamp=add_timestamp)
143
+
144
+ def stop_method(self):
145
+ """Stops the current running method, manual intervention may be needed."""
146
+ self.method_controller.stop()
124
147
 
125
148
  def run_sequence(self, stall_while_running: bool = True):
126
149
  """
127
150
  Starts the currently loaded sequence, storing data
128
- under the <data_dir>/<sequence table name> folder.
151
+ under one of the data_dirs/<sequence table name> folder.
129
152
  Device must be ready.
130
153
 
131
- :param stall_while_running: whether this method should return or stall while HPLC runs.
154
+ :param stall_while_running: whether to return or stall while HPLC runs.
132
155
  """
133
156
  self.sequence_controller.run(stall_while_running=stall_while_running)
134
157
 
158
+ def check_method_complete(self) -> Tuple[float, int]:
159
+ """
160
+ Check if the currently running method (if any) is done.
161
+
162
+ :returns: the percent of the method run completed, and whether the run is complete.
163
+ """
164
+ return self.method_controller.check_hplc_run_finished()
165
+
166
+ def check_sequence_complete(self) -> Tuple[float, int]:
167
+ """
168
+ Check if the currently running sequence (if any) is done.
169
+
170
+ :returns: the percent of the sequence run completed, and whether the run is complete.
171
+ """
172
+ return self.sequence_controller.check_hplc_run_finished()
173
+
135
174
  def edit_method(self, updated_method: MethodDetails, save: bool = False):
136
- """Updated the currently loaded method in ChemStation with provided values.
175
+ """
176
+ Updated the currently loaded method in ChemStation with provided values.
137
177
 
138
178
  :param updated_method: the method with updated values, to be sent to Chemstation to modify the currently loaded method.
139
- :param save: whether this method should be to disk, or just modified.
179
+ :param save: whether this method should be saved to disk, or just modified.
140
180
  """
141
181
  self.method_controller.edit(updated_method, save)
142
182
 
143
183
  def edit_sequence(self, updated_sequence: SequenceTable):
144
184
  """
145
- Updates the currently loaded sequence table with the provided table.
146
- If you would only like to edit a single row of a sequence table, use `edit_sequence_row` instead.
185
+ Updates the currently loaded sequence table with the provided table, and saves the sequence.
147
186
 
148
187
  :param updated_sequence: The sequence table to be written to the currently loaded sequence table.
149
188
  """
150
189
  self.sequence_controller.edit(updated_sequence)
151
190
 
152
- def edit_sequence_row(self, row: SequenceEntry, num: int):
191
+ def get_last_run_method_report(self,
192
+ custom_path: Optional[str] = None,
193
+ report_type: ReportType = ReportType.CSV) -> AgilentReport:
153
194
  """
154
- Edits a row in the sequence table. Assumes the row already exists.
155
-
156
- :param row: sequence row entry with updated information
157
- :param num: the row to edit, based on 1-based indexing
195
+ Return data contained in the REPORT files. Use `aghplctools` if you want more report processing utility.
196
+ :param custom_path: path to sequence folder
197
+ :param report_type: read either the TXT or CSV version
198
+ :returns: report data for method
158
199
  """
159
- self.sequence_controller._edit_row(row, num)
200
+ return self.method_controller.get_report(custom_path=custom_path, report_type=report_type)[0]
160
201
 
161
202
  def get_last_run_method_data(self, read_uv: bool = False,
162
203
  data: Optional[str] = None) -> AgilentChannelChromatogramData:
@@ -168,8 +209,19 @@ class HPLCController:
168
209
  """
169
210
  return self.method_controller.get_data(custom_path=data, read_uv=read_uv)
170
211
 
212
+ def get_last_run_sequence_reports(self,
213
+ custom_path: Optional[str] = None,
214
+ report_type: ReportType = ReportType.CSV) -> List[AgilentReport]:
215
+ """
216
+ Return data contained in the REPORT files. Use `aghplctools` if you want more report processing utility.
217
+ :param custom_path: path to sequence folder
218
+ :param report_type: read either the TXT or CSV version
219
+ :returns: list of reports for each row
220
+ """
221
+ return self.sequence_controller.get_report(custom_path=custom_path, report_type=report_type)
222
+
171
223
  def get_last_run_sequence_data(self, read_uv: bool = False,
172
- data: Optional[str] = None) -> list[AgilentChannelChromatogramData]:
224
+ data: Optional[str] = None) -> List[AgilentChannelChromatogramData]:
173
225
  """
174
226
  Returns data for all rows in the last run sequence data.
175
227
 
@@ -179,30 +231,23 @@ class HPLCController:
179
231
  return self.sequence_controller.get_data(custom_path=data, read_uv=read_uv)
180
232
 
181
233
  def check_loaded_sequence(self) -> str:
182
- """
183
- Returns the name of the currently loaded sequence.
184
- """
234
+ """Returns the name of the currently loaded sequence."""
185
235
  return self.sequence_controller.check()
186
236
 
187
237
  def check_loaded_method(self) -> str:
188
- """
189
- Returns the name of the currently loaded method.
190
- """
238
+ """Returns the name of the currently loaded method."""
191
239
  return self.method_controller.check()
192
240
 
193
241
  def load_injector_program(self) -> InjectorTable:
242
+ """Returns all details of the injector program for the currently loaded method."""
194
243
  return self.method_controller.injector_controller.load()
195
244
 
196
245
  def load_method(self) -> MethodDetails:
197
- """
198
- Returns all details of the currently loaded method, including its timetable.
199
- """
246
+ """Returns details of the currently loaded method, such as its starting modifier conditions and timetable."""
200
247
  return self.method_controller.load()
201
248
 
202
249
  def load_sequence(self) -> SequenceTable:
203
- """
204
- Returns the currently loaded sequence.
205
- """
250
+ """Returns the currently loaded sequence."""
206
251
  return self.sequence_controller.load()
207
252
 
208
253
  def standby(self):
@@ -1,5 +1,5 @@
1
1
  from dataclasses import dataclass, field
2
- from typing import Optional
2
+ from typing import Optional, List
3
3
 
4
4
 
5
5
  @dataclass
@@ -202,8 +202,8 @@ class StopTime:
202
202
 
203
203
  @dataclass
204
204
  class Signals:
205
- signal: list[Signal] = field(
206
- default_factory=list,
205
+ signal: List[Signal] = field(
206
+ default_factory=List,
207
207
  metadata={
208
208
  "name": "Signal",
209
209
  "type": "Element",