pychemstation 0.8.3__py3-none-any.whl → 0.10.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 (55) hide show
  1. pychemstation/analysis/__init__.py +4 -0
  2. pychemstation/analysis/base_spectrum.py +9 -9
  3. pychemstation/analysis/process_report.py +13 -7
  4. pychemstation/analysis/utils.py +1 -3
  5. pychemstation/control/__init__.py +4 -0
  6. pychemstation/control/comm.py +206 -0
  7. pychemstation/control/controllers/__init__.py +6 -0
  8. pychemstation/control/controllers/comm.py +12 -5
  9. pychemstation/control/controllers/devices/column.py +12 -0
  10. pychemstation/control/controllers/devices/dad.py +0 -0
  11. pychemstation/control/controllers/devices/device.py +10 -7
  12. pychemstation/control/controllers/devices/injector.py +18 -84
  13. pychemstation/control/controllers/devices/pump.py +43 -0
  14. pychemstation/control/controllers/method.py +338 -0
  15. pychemstation/control/controllers/sequence.py +190 -0
  16. pychemstation/control/controllers/table_controller.py +266 -0
  17. pychemstation/control/controllers/tables/method.py +35 -13
  18. pychemstation/control/controllers/tables/sequence.py +46 -37
  19. pychemstation/control/controllers/tables/table.py +46 -30
  20. pychemstation/control/hplc.py +27 -11
  21. pychemstation/control/table/__init__.py +3 -0
  22. pychemstation/control/table/method.py +274 -0
  23. pychemstation/control/table/sequence.py +210 -0
  24. pychemstation/control/table/table_controller.py +201 -0
  25. pychemstation/generated/dad_method.py +1 -1
  26. pychemstation/generated/pump_method.py +1 -1
  27. pychemstation/utils/chromatogram.py +2 -5
  28. pychemstation/utils/injector_types.py +1 -1
  29. pychemstation/utils/macro.py +3 -3
  30. pychemstation/utils/method_types.py +2 -2
  31. pychemstation/utils/num_utils.py +65 -0
  32. pychemstation/utils/parsing.py +1 -0
  33. pychemstation/utils/sequence_types.py +3 -3
  34. pychemstation/utils/spec_utils.py +304 -0
  35. {pychemstation-0.8.3.dist-info → pychemstation-0.10.0.dist-info}/METADATA +19 -8
  36. pychemstation-0.10.0.dist-info/RECORD +62 -0
  37. {pychemstation-0.8.3.dist-info → pychemstation-0.10.0.dist-info}/WHEEL +2 -1
  38. pychemstation-0.10.0.dist-info/top_level.txt +2 -0
  39. tests/__init__.py +0 -0
  40. tests/constants.py +134 -0
  41. tests/test_comb.py +136 -0
  42. tests/test_comm.py +65 -0
  43. tests/test_inj.py +39 -0
  44. tests/test_method.py +99 -0
  45. tests/test_nightly.py +80 -0
  46. tests/test_offline_stable.py +69 -0
  47. tests/test_online_stable.py +275 -0
  48. tests/test_proc_rep.py +52 -0
  49. tests/test_runs_stable.py +225 -0
  50. tests/test_sequence.py +125 -0
  51. tests/test_stable.py +276 -0
  52. pychemstation/control/README.md +0 -124
  53. pychemstation/control/controllers/README.md +0 -1
  54. pychemstation-0.8.3.dist-info/RECORD +0 -37
  55. {pychemstation-0.8.3.dist-info → pychemstation-0.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,338 @@
1
+ import os
2
+ import time
3
+ from typing import Optional, Union
4
+
5
+ from xsdata.formats.dataclass.parsers import XmlParser
6
+
7
+ from ...control.controllers.table_controller import TableController
8
+ from ...control.controllers.comm import CommunicationController
9
+ from ...generated import PumpMethod, DadMethod, SolventElement
10
+ from ...utils.chromatogram import TIME_FORMAT, AgilentChannelChromatogramData
11
+ from ...utils.macro import Command
12
+ from ...utils.method_types import PType, TimeTableEntry, Param, MethodDetails, HPLCMethodParams
13
+ from ...utils.table_types import RegisterFlag, TableOperation, Table
14
+
15
+
16
+ class MethodController(TableController):
17
+ """
18
+ Class containing method related logic
19
+ """
20
+
21
+ def __init__(self, controller: CommunicationController, src: str, data_dir: str, table: Table):
22
+ super().__init__(controller, src, data_dir, table)
23
+
24
+ def check(self) -> str:
25
+ time.sleep(2)
26
+ self.send(Command.GET_METHOD_CMD)
27
+ time.sleep(2)
28
+ res = self.receive()
29
+ if res.is_ok():
30
+ return res.ok_value.string_response
31
+ return "ERROR"
32
+
33
+ def get_method_params(self) -> HPLCMethodParams:
34
+ return HPLCMethodParams(organic_modifier=self.controller.get_num_val(
35
+ cmd=TableOperation.GET_OBJ_HDR_VAL.value.format(
36
+ register=self.table.register,
37
+ register_flag=RegisterFlag.SOLVENT_B_COMPOSITION
38
+ )
39
+ ),
40
+ flow=self.controller.get_num_val(
41
+ cmd=TableOperation.GET_OBJ_HDR_VAL.value.format(
42
+ register=self.table.register,
43
+ register_flag=RegisterFlag.FLOW
44
+ )
45
+ ),
46
+ )
47
+
48
+ def get_row(self, row: int) -> TimeTableEntry:
49
+ return TimeTableEntry(start_time=self.get_num(row, RegisterFlag.TIME),
50
+ organic_modifer=self.get_num(row, RegisterFlag.TIMETABLE_SOLVENT_B_COMPOSITION),
51
+ flow=self.get_num(row, RegisterFlag.TIMETABLE_FLOW))
52
+
53
+ def get_timetable(self, rows: int):
54
+ uncoalesced_timetable_rows = [self.get_row(r + 1) for r in range(rows)]
55
+ timetable_rows = {}
56
+ for row in uncoalesced_timetable_rows:
57
+ time_key = str(row.start_time)
58
+ if time_key not in timetable_rows.keys():
59
+ timetable_rows[time_key] = TimeTableEntry(start_time=row.start_time,
60
+ flow=row.flow,
61
+ organic_modifer=row.organic_modifer)
62
+ else:
63
+ if row.flow:
64
+ timetable_rows[time_key].flow = row.flow
65
+ if row.organic_modifer:
66
+ timetable_rows[time_key].organic_modifer = row.organic_modifer
67
+ entries = list(timetable_rows.values())
68
+ entries.sort(key=lambda e: e.start_time)
69
+ return entries
70
+
71
+ def load(self) -> MethodDetails:
72
+ rows = self.get_num_rows()
73
+ if rows.is_ok():
74
+ self.send(Command.GET_METHOD_CMD)
75
+ res = self.receive()
76
+ method_name = res.ok_value.string_response
77
+ timetable_rows = self.get_timetable(int(rows.ok_value.num_response))
78
+ params = self.get_method_params()
79
+ stop_time = self.controller.get_num_val(
80
+ cmd=TableOperation.GET_OBJ_HDR_VAL.value.format(
81
+ register=self.table.register,
82
+ register_flag=RegisterFlag.MAX_TIME))
83
+ post_time = self.controller.get_num_val(
84
+ cmd=TableOperation.GET_OBJ_HDR_VAL.value.format(
85
+ register=self.table.register,
86
+ register_flag=RegisterFlag.POST_TIME))
87
+ self.table_state = MethodDetails(
88
+ name=method_name,
89
+ timetable=timetable_rows,
90
+ stop_time=stop_time,
91
+ post_time=post_time,
92
+ params=params)
93
+ return self.table_state
94
+ else:
95
+ raise RuntimeError(rows.err_value)
96
+
97
+ def current_method(self, method_name: str):
98
+ """
99
+ Checks if a given method is already loaded into Chemstation. Method name does not need the ".M" extension.
100
+
101
+ :param method_name: a Chemstation method
102
+ :return: True if method is already loaded
103
+ """
104
+ self.send(Command.GET_METHOD_CMD)
105
+ parsed_response = self.receive()
106
+ return method_name in parsed_response
107
+
108
+ def switch(self, method_name: str):
109
+ """
110
+ Allows the user to switch between pre-programmed methods. No need to append '.M'
111
+ to the end of the method name. For example. for the method named 'General-Poroshell.M',
112
+ only 'General-Poroshell' is needed.
113
+
114
+ :param method_name: any available method in Chemstation method directory
115
+ :raise IndexError: Response did not have expected format. Try again.
116
+ :raise AssertionError: The desired method is not selected. Try again.
117
+ """
118
+ self.send(Command.SWITCH_METHOD_CMD.value.format(method_dir=self.src,
119
+ method_name=method_name))
120
+
121
+ time.sleep(2)
122
+ self.send(Command.GET_METHOD_CMD)
123
+ time.sleep(2)
124
+ res = self.receive()
125
+ if res.is_ok():
126
+ parsed_response = res.ok_value.string_response
127
+ assert parsed_response == f"{method_name}.M", "Switching Methods failed."
128
+ self.table_state = None
129
+
130
+ def load_from_disk(self, method_name: str) -> MethodDetails:
131
+ """
132
+ Retrieve method details of an existing method. Don't need to append ".M" to the end. This assumes the
133
+ organic modifier is in Channel B and that Channel A contains the aq layer. Additionally, assumes
134
+ only two solvents are being used.
135
+
136
+ :param method_name: name of method to load details of
137
+ :raises FileNotFoundError: Method does not exist
138
+ :return: method details
139
+ """
140
+ method_folder = f"{method_name}.M"
141
+ method_path = os.path.join(self.src, method_folder, "AgilentPumpDriver1.RapidControl.MethodXML.xml")
142
+ dad_path = os.path.join(self.src, method_folder, "Agilent1200erDadDriver1.RapidControl.MethodXML.xml")
143
+
144
+ if os.path.exists(os.path.join(self.src, f"{method_name}.M")):
145
+ parser = XmlParser()
146
+ method = parser.parse(method_path, PumpMethod)
147
+ dad = parser.parse(dad_path, DadMethod)
148
+
149
+ organic_modifier: Optional[SolventElement] = None
150
+ aq_modifier: Optional[SolventElement] = None
151
+
152
+ if len(method.solvent_composition.solvent_element) == 2:
153
+ for solvent in method.solvent_composition.solvent_element:
154
+ if solvent.channel == "Channel_A":
155
+ aq_modifier = solvent
156
+ elif solvent.channel == "Channel_B":
157
+ organic_modifier = solvent
158
+
159
+ self.table_state = MethodDetails(
160
+ name=method_name,
161
+ params=HPLCMethodParams(organic_modifier=organic_modifier.percentage,
162
+ flow=method.flow),
163
+ stop_time=method.stop_time.stop_time_value,
164
+ post_time=method.post_time.post_time_value,
165
+ timetable=[TimeTableEntry(start_time=tte.time,
166
+ organic_modifer=tte.percent_b,
167
+ flow=method.flow
168
+ ) for tte in method.timetable.timetable_entry],
169
+ dad_wavelengthes=dad.signals.signal)
170
+ return self.table_state
171
+ else:
172
+ raise FileNotFoundError
173
+
174
+ def edit(self, updated_method: MethodDetails, save: bool):
175
+ """Updated the currently loaded method in ChemStation with provided values.
176
+
177
+ :param updated_method: the method with updated values, to be sent to Chemstation to modify the currently loaded method.
178
+ :param save: if false only modifies the method, otherwise saves to disk
179
+ """
180
+ self.table_state = updated_method
181
+ initial_organic_modifier: Param = Param(val=updated_method.params.organic_modifier,
182
+ chemstation_key=RegisterFlag.SOLVENT_B_COMPOSITION,
183
+ ptype=PType.NUM)
184
+ max_time: Param = Param(val=updated_method.stop_time,
185
+ chemstation_key=RegisterFlag.MAX_TIME,
186
+ ptype=PType.NUM)
187
+ post_time: Param = Param(val=updated_method.post_time,
188
+ chemstation_key=RegisterFlag.POST_TIME,
189
+ ptype=PType.NUM) # TODO check postime works
190
+ flow: Param = Param(val=updated_method.params.flow,
191
+ chemstation_key=RegisterFlag.FLOW,
192
+ ptype=PType.NUM)
193
+
194
+ # Method settings required for all runs
195
+ self.update_method_params(flow, initial_organic_modifier, max_time, post_time)
196
+ self._update_method_timetable(updated_method.timetable)
197
+
198
+ if save:
199
+ self.send(Command.SAVE_METHOD_CMD.value.format(
200
+ commit_msg=f"saved method at {str(time.time())}"
201
+ ))
202
+
203
+ def update_method_params(self, flow, initial_organic_modifier, max_time, post_time):
204
+ self.delete_table()
205
+ self._update_param(initial_organic_modifier)
206
+ self._update_param(flow)
207
+ if self.table_state.stop_time:
208
+ self._update_param(Param(val="Set",
209
+ chemstation_key=RegisterFlag.STOPTIME_MODE,
210
+ ptype=PType.STR))
211
+ self._update_param(max_time)
212
+ else:
213
+ self._update_param(Param(val="Off",
214
+ chemstation_key=RegisterFlag.STOPTIME_MODE,
215
+ ptype=PType.STR))
216
+ if self.table_state.post_time:
217
+ self._update_param(Param(val="Set",
218
+ chemstation_key=RegisterFlag.POSTIME_MODE,
219
+ ptype=PType.STR))
220
+ self._update_param(post_time)
221
+ self.download()
222
+
223
+ def _update_param(self, method_param: Param):
224
+ """Change a method parameter, changes what is visibly seen in Chemstation GUI.
225
+ (changes the first row in the timetable)
226
+
227
+ :param method_param: a parameter to update for currently loaded method.
228
+ """
229
+ register = self.table.register
230
+ setting_command = TableOperation.UPDATE_OBJ_HDR_VAL if method_param.ptype == PType.NUM else TableOperation.UPDATE_OBJ_HDR_TEXT
231
+ if isinstance(method_param.chemstation_key, list):
232
+ for register_flag in method_param.chemstation_key:
233
+ self.send(setting_command.value.format(register=register,
234
+ register_flag=register_flag,
235
+ val=method_param.val))
236
+ else:
237
+ self.send(setting_command.value.format(register=register,
238
+ register_flag=method_param.chemstation_key,
239
+ val=method_param.val))
240
+ time.sleep(2)
241
+
242
+ def download(self):
243
+ self.send('Sleep 1')
244
+ self.sleepy_send("DownloadRCMethod PMP1")
245
+ self.send('Sleep 1')
246
+
247
+ def edit_row(self, row: TimeTableEntry, first_row: bool = False):
248
+ if first_row:
249
+ self.add_row()
250
+ self.add_new_col_text(col_name=RegisterFlag.FUNCTION,
251
+ val=RegisterFlag.SOLVENT_COMPOSITION.value)
252
+ self.add_new_col_num(col_name=RegisterFlag.TIME, val=row.start_time)
253
+ self.add_new_col_num(col_name=RegisterFlag.TIMETABLE_SOLVENT_B_COMPOSITION,
254
+ val=row.organic_modifer)
255
+ self.add_row()
256
+ self.get_num_rows()
257
+ self.edit_row_text(col_name=RegisterFlag.FUNCTION, val=RegisterFlag.FLOW.value)
258
+ self.add_new_col_num(col_name=RegisterFlag.TIMETABLE_FLOW, val=row.flow)
259
+ self.edit_row_num(col_name=RegisterFlag.TIMETABLE_FLOW, val=row.flow)
260
+ self.download()
261
+ else:
262
+ self.add_row()
263
+ self.get_num_rows()
264
+ self.edit_row_text(col_name=RegisterFlag.FUNCTION,
265
+ val=RegisterFlag.SOLVENT_COMPOSITION.value)
266
+ self.edit_row_num(col_name=RegisterFlag.TIME, val=row.start_time)
267
+ self.edit_row_num(col_name=RegisterFlag.TIMETABLE_SOLVENT_B_COMPOSITION,
268
+ val=row.organic_modifer)
269
+ self.download()
270
+
271
+ self.add_row()
272
+ self.get_num_rows()
273
+ self.edit_row_text(col_name=RegisterFlag.FUNCTION, val=RegisterFlag.FLOW.value)
274
+ self.edit_row_num(col_name=RegisterFlag.TIMETABLE_FLOW, val=row.flow)
275
+ self.edit_row_num(col_name=RegisterFlag.TIME, val=row.start_time)
276
+ self.download()
277
+
278
+ def _update_method_timetable(self, timetable_rows: list[TimeTableEntry]):
279
+ self.get_num_rows()
280
+
281
+ self.delete_table()
282
+ res = self.get_num_rows()
283
+ while not res.is_err():
284
+ self.delete_table()
285
+ res = self.get_num_rows()
286
+
287
+ self.new_table()
288
+ self.get_num_rows()
289
+
290
+ for i, row in enumerate(timetable_rows):
291
+ self.edit_row(row=row, first_row=i == 0)
292
+
293
+ def stop(self):
294
+ """
295
+ Stops the method run. A dialog window will pop up and manual intervention may be required.\
296
+ """
297
+ self.send(Command.STOP_METHOD_CMD)
298
+
299
+ def run(self, experiment_name: str, stall_while_running: bool = True):
300
+ """
301
+ This is the preferred method to trigger a run.
302
+ Starts the currently selected method, storing data
303
+ under the <data_dir>/<experiment_name>.D folder.
304
+ The <experiment_name> will be appended with a timestamp in the '%Y-%m-%d-%H-%M' format.
305
+ Device must be ready.
306
+
307
+ :param experiment_name: Name of the experiment
308
+ """
309
+ if not self.table_state:
310
+ self.table_state = self.load()
311
+
312
+ timestamp = time.strftime(TIME_FORMAT)
313
+ self.send(Command.RUN_METHOD_CMD.value.format(data_dir=self.data_dir,
314
+ experiment_name=experiment_name,
315
+ timestamp=timestamp))
316
+
317
+ if self.check_hplc_is_running():
318
+ folder_name = f"{experiment_name}_{timestamp}.D"
319
+ self.data_files.append(os.path.join(self.data_dir, folder_name))
320
+
321
+ if stall_while_running:
322
+ run_completed = self.check_hplc_done_running(method=self.table_state)
323
+ if run_completed.is_ok():
324
+ self.data_files[-1] = run_completed.value
325
+ else:
326
+ raise RuntimeError("Run error has occurred.")
327
+ else:
328
+ self.data_files[-1].dir = self.fuzzy_match_most_recent_folder(folder_name).ok_value
329
+
330
+ def retrieve_recent_data_files(self) -> str:
331
+ return self.data_files[-1]
332
+
333
+ def get_data(self, custom_path: Optional[str] = None) -> AgilentChannelChromatogramData:
334
+ if not custom_path:
335
+ self.get_spectrum(self.data_files[-1])
336
+ else:
337
+ self.get_spectrum(custom_path)
338
+ return AgilentChannelChromatogramData(**self.spectra)
@@ -0,0 +1,190 @@
1
+ import os
2
+ import time
3
+ from typing import Optional
4
+
5
+ from ...control.controllers.comm import CommunicationController
6
+ from ...control.controllers.table_controller import TableController
7
+ from ...utils.chromatogram import SEQUENCE_TIME_FORMAT, AgilentChannelChromatogramData
8
+ from ...utils.macro import Command
9
+ from ...utils.sequence_types import SequenceTable, SequenceEntry, SequenceDataFiles, InjectionSource, SampleType
10
+ from ...utils.table_types import RegisterFlag, Table
11
+ from ...utils.tray_types import TenVialColumn
12
+
13
+
14
+ class SequenceController(TableController):
15
+ """
16
+ Class containing sequence related logic
17
+ """
18
+
19
+ def __init__(self, controller: CommunicationController, src: str, data_dir: str, table: Table, method_dir: str):
20
+ self.method_dir = method_dir
21
+ super().__init__(controller, src, data_dir, table)
22
+
23
+ def load(self) -> SequenceTable:
24
+ rows = self.get_num_rows()
25
+ self.send(Command.GET_SEQUENCE_CMD)
26
+ seq_name = self.receive()
27
+
28
+ if rows.is_ok() and seq_name.is_ok():
29
+ self.table_state = SequenceTable(
30
+ name=seq_name.ok_value.string_response.partition(".S")[0],
31
+ rows=[self.get_row(r + 1) for r in range(int(rows.ok_value.num_response))])
32
+ return self.table_state
33
+ raise RuntimeError(rows.err_value)
34
+
35
+ def get_row(self, row: int) -> SequenceEntry:
36
+ sample_name = self.get_text(row, RegisterFlag.NAME)
37
+ vial_location = int(self.get_num(row, RegisterFlag.VIAL_LOCATION))
38
+ method = self.get_text(row, RegisterFlag.METHOD)
39
+ num_inj = int(self.get_num(row, RegisterFlag.NUM_INJ))
40
+ inj_vol = int(self.get_text(row, RegisterFlag.INJ_VOL))
41
+ inj_source = InjectionSource(self.get_text(row, RegisterFlag.INJ_SOR))
42
+ sample_type = SampleType(self.get_num(row, RegisterFlag.SAMPLE_TYPE))
43
+ return SequenceEntry(sample_name=sample_name,
44
+ vial_location=vial_location,
45
+ method=None if len(method) == 0 else method,
46
+ num_inj=num_inj,
47
+ inj_vol=inj_vol,
48
+ inj_source=inj_source,
49
+ sample_type=sample_type)
50
+
51
+ def check(self) -> str:
52
+ time.sleep(2)
53
+ self.send(Command.GET_SEQUENCE_CMD)
54
+ time.sleep(2)
55
+ res = self.receive()
56
+ if res.is_ok():
57
+ return res.ok_value.string_response
58
+ return "ERROR"
59
+
60
+ def switch(self, seq_name: str):
61
+ """
62
+ Switch to the specified sequence. The sequence name does not need the '.S' extension.
63
+
64
+ :param seq_name: The name of the sequence file
65
+ """
66
+ self.send(f'_SeqFile$ = "{seq_name}.S"')
67
+ self.send(f'_SeqPath$ = "{self.src}"')
68
+ self.send(Command.SWITCH_SEQUENCE_CMD)
69
+ time.sleep(2)
70
+ self.send(Command.GET_SEQUENCE_CMD)
71
+ time.sleep(2)
72
+ parsed_response = self.receive().value.string_response
73
+
74
+ assert parsed_response == f"{seq_name}.S", "Switching sequence failed."
75
+ self.table_state = None
76
+
77
+ def edit(self, sequence_table: SequenceTable):
78
+ """
79
+ Updates the currently loaded sequence table with the provided table. This method will delete the existing sequence table and remake it.
80
+ If you would only like to edit a single row of a sequence table, use `edit_sequence_table_row` instead.
81
+
82
+ :param sequence_table:
83
+ """
84
+ self.table_state = sequence_table
85
+ rows = self.get_num_rows()
86
+ if rows.is_ok():
87
+ existing_row_num = rows.value.num_response
88
+ wanted_row_num = len(sequence_table.rows)
89
+ while existing_row_num != wanted_row_num:
90
+ if wanted_row_num > existing_row_num:
91
+ self.add_row()
92
+ elif wanted_row_num < existing_row_num:
93
+ self.delete_row(int(existing_row_num))
94
+ self.send(Command.SAVE_SEQUENCE_CMD)
95
+ existing_row_num = self.get_num_rows().ok_value.num_response
96
+ self.send(Command.SWITCH_SEQUENCE_CMD)
97
+
98
+ for i, row in enumerate(sequence_table.rows):
99
+ self.edit_row(row=row, row_num=i + 1)
100
+ self.sleep(1)
101
+ self.send(Command.SAVE_SEQUENCE_CMD)
102
+ self.send(Command.SWITCH_SEQUENCE_CMD)
103
+
104
+ def edit_row(self, row: SequenceEntry, row_num: int):
105
+ """
106
+ Edits a row in the sequence table. If a row does NOT exist, a new one will be created.
107
+
108
+ :param row: sequence row entry with updated information
109
+ :param row_num: the row to edit, based on 1-based indexing
110
+ """
111
+ num_rows = self.get_num_rows()
112
+ if num_rows.is_ok():
113
+ while num_rows.ok_value.num_response < row_num:
114
+ self.add_row()
115
+ self.send(Command.SAVE_SEQUENCE_CMD)
116
+ num_rows = self.get_num_rows()
117
+
118
+ if row.vial_location:
119
+ loc = row.vial_location
120
+ if isinstance(row.vial_location, TenVialColumn):
121
+ loc = row.vial_location.value
122
+ self.edit_row_num(row=row_num, col_name=RegisterFlag.VIAL_LOCATION, val=loc)
123
+
124
+ if row.method:
125
+ possible_path = os.path.join(self.method_dir, row.method) + ".M\\"
126
+ method = row.method
127
+ if os.path.exists(possible_path):
128
+ method = os.path.join(self.method_dir, row.method)
129
+ self.edit_row_text(row=row_num, col_name=RegisterFlag.METHOD, val=method)
130
+
131
+ if row.num_inj:
132
+ self.edit_row_num(row=row_num, col_name=RegisterFlag.NUM_INJ, val=row.num_inj)
133
+
134
+ if row.inj_vol:
135
+ self.edit_row_text(row=row_num, col_name=RegisterFlag.INJ_VOL, val=row.inj_vol)
136
+
137
+ if row.inj_source:
138
+ self.edit_row_text(row=row_num, col_name=RegisterFlag.INJ_SOR, val=row.inj_source.value)
139
+
140
+ if row.sample_name:
141
+ self.edit_row_text(row=row_num, col_name=RegisterFlag.NAME, val=row.sample_name)
142
+ self.edit_row_text(row=row_num, col_name=RegisterFlag.DATA_FILE, val=row.sample_name)
143
+
144
+ if row.sample_type:
145
+ self.edit_row_num(row=row_num, col_name=RegisterFlag.SAMPLE_TYPE, val=row.sample_type.value)
146
+
147
+ self.send(Command.SAVE_SEQUENCE_CMD)
148
+
149
+ def run(self, stall_while_running: bool = True):
150
+ """
151
+ Starts the currently loaded sequence, storing data
152
+ under the <data_dir>/<sequence table name> folder.
153
+ Device must be ready.
154
+ """
155
+ if not self.table_state:
156
+ self.table_state = self.load()
157
+
158
+ timestamp = time.strftime(SEQUENCE_TIME_FORMAT)
159
+ self.send(Command.RUN_SEQUENCE_CMD.value)
160
+
161
+ if self.check_hplc_is_running():
162
+ folder_name = f"{self.table_state.name} {timestamp}"
163
+ self.data_files.append(SequenceDataFiles(dir=folder_name,
164
+ sequence_name=self.table_state.name))
165
+
166
+ if stall_while_running:
167
+ run_completed = self.check_hplc_done_running(sequence=self.table_state)
168
+ if run_completed.is_ok():
169
+ self.data_files[-1].dir = run_completed.value
170
+ else:
171
+ raise RuntimeError("Run error has occurred.")
172
+ else:
173
+ self.data_files[-1].dir = self.fuzzy_match_most_recent_folder(folder_name).ok_value
174
+
175
+ def retrieve_recent_data_files(self) -> str:
176
+ sequence_data_files: SequenceDataFiles = self.data_files[-1]
177
+ return sequence_data_files.dir
178
+
179
+ def get_data(self, custom_path: Optional[str] = None) -> list[AgilentChannelChromatogramData]:
180
+ parent_dir = self.data_files[-1].dir if not custom_path else custom_path
181
+ subdirs = [x[0] for x in os.walk(self.data_dir)]
182
+ potential_folders = sorted(list(filter(lambda d: parent_dir in d, subdirs)))
183
+ self.data_files[-1].child_dirs = [f for f in potential_folders if
184
+ parent_dir in f and ".M" not in f and ".D" in f]
185
+
186
+ spectra: list[AgilentChannelChromatogramData] = []
187
+ for row in self.data_files[-1].child_dirs:
188
+ self.get_spectrum(row)
189
+ spectra.append(AgilentChannelChromatogramData(**self.spectra))
190
+ return spectra