pychemstation 0.4.7.dev2__py3-none-any.whl → 0.5.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.
@@ -0,0 +1,3 @@
1
+ from .method import MethodController
2
+ from .sequence import SequenceController
3
+ from .table_controller import TableController
@@ -0,0 +1,258 @@
1
+ import os
2
+ import time
3
+ from typing import Optional, Any
4
+
5
+ from xsdata.formats.dataclass.parsers import XmlParser
6
+
7
+ from .. import CommunicationController
8
+ from ...control.table.table_controller import TableController
9
+ from ...generated import PumpMethod, DadMethod, SolventElement
10
+ from ...utils.chromatogram import TIME_FORMAT
11
+ from ...utils.constants import METHOD_TIMETABLE
12
+ from ...utils.macro import Command
13
+ from ...utils.method_types import PType, TimeTableEntry, Param, MethodTimetable, HPLCMethodParams
14
+ from ...utils.table_types import RegisterFlag, TableOperation
15
+
16
+
17
+ class MethodController(TableController):
18
+ """
19
+ Class containing method related logic
20
+ """
21
+
22
+ def __init__(self, controller: CommunicationController, src: str, data_dir: str):
23
+ super().__init__(controller, src, data_dir)
24
+
25
+ def is_loaded(self, method_name: str):
26
+ """
27
+ Checks if a given method is already loaded into Chemstation. Method name does not need the ".M" extension.
28
+
29
+ :param method_name: a Chemstation method
30
+ :return: True if method is already loaded
31
+ """
32
+ self.send(Command.GET_METHOD_CMD)
33
+ parsed_response = self.receive().splitlines()[1].split()[1:][0]
34
+ return method_name in parsed_response
35
+
36
+ def switch(self, method_name: str):
37
+ """
38
+ Allows the user to switch between pre-programmed methods. No need to append '.M'
39
+ to the end of the method name. For example. for the method named 'General-Poroshell.M',
40
+ only 'General-Poroshell' is needed.
41
+
42
+ :param method_name: any available method in Chemstation method directory
43
+ :raise IndexError: Response did not have expected format. Try again.
44
+ :raise AssertionError: The desired method is not selected. Try again.
45
+ """
46
+ self.send(Command.SWITCH_METHOD_CMD.value.format(method_dir=self.src,
47
+ method_name=method_name))
48
+
49
+ time.sleep(2)
50
+ self.send(Command.GET_METHOD_CMD)
51
+ time.sleep(2)
52
+
53
+ # check that method switched
54
+ for _ in range(10):
55
+ try:
56
+ parsed_response = self.receive().splitlines()[1].split()[1:][0]
57
+ break
58
+ except IndexError:
59
+ continue
60
+
61
+ assert parsed_response == f"{method_name}.M", "Switching Methods failed."
62
+
63
+ def load(self, method_name: str):
64
+ """
65
+ Retrieve method details of an existing method. Don't need to append ".M" to the end. This assumes the
66
+ organic modifier is in Channel B and that Channel A contains the aq layer. Additionally, assumes
67
+ only two solvents are being used.
68
+
69
+ :param method_name: name of method to load details of
70
+ :raises FileNotFoundError: Method does not exist
71
+ :return: method details
72
+ """
73
+ method_folder = f"{method_name}.M"
74
+ method_path = os.path.join(self.src, method_folder, "AgilentPumpDriver1.RapidControl.MethodXML.xml")
75
+ dad_path = os.path.join(self.src, method_folder, "Agilent1200erDadDriver1.RapidControl.MethodXML.xml")
76
+
77
+ if os.path.exists(os.path.join(self.src, f"{method_name}.M")):
78
+ parser = XmlParser()
79
+ method = parser.parse(method_path, PumpMethod)
80
+ dad = parser.parse(dad_path, DadMethod)
81
+
82
+ organic_modifier: Optional[SolventElement] = None
83
+ aq_modifier: Optional[SolventElement] = None
84
+
85
+ if len(method.solvent_composition.solvent_element) == 2:
86
+ for solvent in method.solvent_composition.solvent_element:
87
+ if solvent.channel == "Channel_A":
88
+ aq_modifier = solvent
89
+ elif solvent.channel == "Channel_B":
90
+ organic_modifier = solvent
91
+
92
+ return MethodTimetable(
93
+ first_row=HPLCMethodParams(
94
+ organic_modifier=Param(val=organic_modifier.percentage,
95
+ chemstation_key=RegisterFlag.SOLVENT_B_COMPOSITION,
96
+ ptype=PType.NUM),
97
+ flow=Param(val=method.flow,
98
+ chemstation_key=RegisterFlag.FLOW,
99
+ ptype=PType.NUM),
100
+ maximum_run_time=Param(val=method.stop_time,
101
+ chemstation_key=RegisterFlag.MAX_TIME,
102
+ ptype=PType.NUM),
103
+ temperature=Param(val=None,
104
+ chemstation_key=[RegisterFlag.COLUMN_OVEN_TEMP1,
105
+ RegisterFlag.COLUMN_OVEN_TEMP2],
106
+ ptype=PType.NUM),
107
+ inj_vol=Param(val=None,
108
+ chemstation_key=None,
109
+ ptype=PType.NUM),
110
+ equ_time=Param(val=None,
111
+ chemstation_key=None,
112
+ ptype=PType.NUM)),
113
+ subsequent_rows=[
114
+ TimeTableEntry(
115
+ start_time=tte.time,
116
+ organic_modifer=tte.percent_b,
117
+ flow=method.flow
118
+ ) for tte in method.timetable.timetable_entry
119
+ ],
120
+ dad_wavelengthes=dad.signals.signal,
121
+ organic_modifier=organic_modifier,
122
+ modifier_a=aq_modifier
123
+ )
124
+ else:
125
+ raise FileNotFoundError
126
+
127
+ def edit(self, updated_method: MethodTimetable):
128
+ """Updated the currently loaded method in ChemStation with provided values.
129
+
130
+ :param updated_method: the method with updated values, to be sent to Chemstation to modify the currently loaded method.
131
+ """
132
+ initial_organic_modifier: Param = Param(val=updated_method.first_row.organic_modifier,
133
+ chemstation_key=RegisterFlag.SOLVENT_B_COMPOSITION,
134
+ ptype=PType.NUM)
135
+ max_time: Param = Param(val=updated_method.first_row.maximum_run_time,
136
+ chemstation_key=RegisterFlag.MAX_TIME,
137
+ ptype=PType.NUM)
138
+ flow: Param = Param(val=updated_method.first_row.flow,
139
+ chemstation_key=RegisterFlag.FLOW,
140
+ ptype=PType.NUM)
141
+ temperature: Param = Param(val=updated_method.first_row.temperature,
142
+ chemstation_key=[RegisterFlag.COLUMN_OVEN_TEMP1,
143
+ RegisterFlag.COLUMN_OVEN_TEMP2],
144
+ ptype=PType.NUM)
145
+
146
+ # Method settings required for all runs
147
+ self.delete_table(METHOD_TIMETABLE)
148
+ self._update_param(initial_organic_modifier)
149
+ self._update_param(flow)
150
+ self._update_param(Param(val="Set",
151
+ chemstation_key=RegisterFlag.STOPTIME_MODE,
152
+ ptype=PType.STR))
153
+ self._update_param(max_time)
154
+ self._update_param(Param(val="Off",
155
+ chemstation_key=RegisterFlag.POSTIME_MODE,
156
+ ptype=PType.STR))
157
+
158
+ self.send("DownloadRCMethod PMP1")
159
+
160
+ self._update_method_timetable(updated_method.subsequent_rows)
161
+
162
+ def _update_method_timetable(self, timetable_rows: list[TimeTableEntry]):
163
+ self.sleepy_send('Local Rows')
164
+ self._get_table_rows(METHOD_TIMETABLE)
165
+
166
+ self.sleepy_send('DelTab RCPMP1Method[1], "Timetable"')
167
+ res = self._get_table_rows(METHOD_TIMETABLE)
168
+ while "ERROR" not in res:
169
+ self.sleepy_send('DelTab RCPMP1Method[1], "Timetable"')
170
+ res = self._get_table_rows(METHOD_TIMETABLE)
171
+
172
+ self.sleepy_send('NewTab RCPMP1Method[1], "Timetable"')
173
+ self._get_table_rows(METHOD_TIMETABLE)
174
+
175
+ for i, row in enumerate(timetable_rows):
176
+ if i == 0:
177
+ self.send('Sleep 1')
178
+ self.sleepy_send('InsTabRow RCPMP1Method[1], "Timetable"')
179
+ self.send('Sleep 1')
180
+
181
+ self.sleepy_send('NewColText RCPMP1Method[1], "Timetable", "Function", "SolventComposition"')
182
+ self.sleepy_send(f'NewColVal RCPMP1Method[1], "Timetable", "Time", {row.start_time}')
183
+ self.sleepy_send(
184
+ f'NewColVal RCPMP1Method[1], "Timetable", "SolventCompositionPumpChannel2_Percentage", {row.organic_modifer}')
185
+
186
+ self.send('Sleep 1')
187
+ self.sleepy_send("DownloadRCMethod PMP1")
188
+ self.send('Sleep 1')
189
+ else:
190
+ self.sleepy_send('InsTabRow RCPMP1Method[1], "Timetable"')
191
+ self._get_table_rows(METHOD_TIMETABLE)
192
+
193
+ self.sleepy_send(
194
+ f'SetTabText RCPMP1Method[1], "Timetable", Rows, "Function", "SolventComposition"')
195
+ self.sleepy_send(
196
+ f'SetTabVal RCPMP1Method[1], "Timetable", Rows, "Time", {row.start_time}')
197
+ self.sleepy_send(
198
+ f'SetTabVal RCPMP1Method[1], "Timetable", Rows, "SolventCompositionPumpChannel2_Percentage", {row.organic_modifer}')
199
+
200
+ self.send("Sleep 1")
201
+ self.sleepy_send("DownloadRCMethod PMP1")
202
+ self.send("Sleep 1")
203
+ self._get_table_rows(METHOD_TIMETABLE)
204
+
205
+ def _update_param(self, method_param: Param):
206
+ """Change a method parameter, changes what is visibly seen in Chemstation GUI.
207
+ (changes the first row in the timetable)
208
+
209
+ :param method_param: a parameter to update for currently loaded method.
210
+ """
211
+ register = METHOD_TIMETABLE.register
212
+ setting_command = TableOperation.UPDATE_OBJ_HDR_VAL if method_param.ptype == PType.NUM else TableOperation.UPDATE_OBJ_HDR_TEXT
213
+ if isinstance(method_param.chemstation_key, list):
214
+ for register_flag in method_param.chemstation_key:
215
+ self.send(setting_command.value.format(register=register,
216
+ register_flag=register_flag,
217
+ val=method_param.val))
218
+ else:
219
+ self.send(setting_command.value.format(register=register,
220
+ register_flag=method_param.chemstation_key,
221
+ val=method_param.val))
222
+ time.sleep(2)
223
+
224
+ def stop(self):
225
+ """
226
+ Stops the method run. A dialog window will pop up and manual intervention may be required.\
227
+ """
228
+ self.send(Command.STOP_METHOD_CMD)
229
+
230
+ def run(self, experiment_name: str):
231
+ """
232
+ This is the preferred method to trigger a run.
233
+ Starts the currently selected method, storing data
234
+ under the <data_dir>/<experiment_name>.D folder.
235
+ The <experiment_name> will be appended with a timestamp in the '%Y-%m-%d-%H-%M' format.
236
+ Device must be ready.
237
+
238
+ :param experiment_name: Name of the experiment
239
+ """
240
+ timestamp = time.strftime(TIME_FORMAT)
241
+
242
+ self.send(Command.RUN_METHOD_CMD.value.format(data_dir=self.data_dir,
243
+ experiment_name=experiment_name,
244
+ timestamp=timestamp))
245
+
246
+ folder_name = f"{experiment_name}_{timestamp}.D"
247
+ self.data_files.append(os.path.join(self.data_dir, folder_name))
248
+
249
+ def retrieve_recent_data_files(self) -> str:
250
+ return self.data_files[-1]
251
+
252
+ def get_data(self) -> tuple[bool, Any]:
253
+ data_ready = self.data_ready()
254
+ if data_ready:
255
+ self.get_spectrum(self.data_files[-1])
256
+ return data_ready, self.spectra
257
+ else:
258
+ return False, None
@@ -0,0 +1,170 @@
1
+ from typing import Any
2
+
3
+ from copy import deepcopy
4
+
5
+ import os
6
+ import time
7
+
8
+ from .table_controller import TableController
9
+ from ...control import CommunicationController
10
+ from ...utils.chromatogram import SEQUENCE_TIME_FORMAT, AgilentHPLCChromatogram
11
+ from ...utils.constants import SEQUENCE_TABLE
12
+ from ...utils.macro import Command
13
+ from ...utils.sequence_types import SequenceTable, SequenceEntry, SequenceDataFiles
14
+ from ...utils.table_types import TableOperation, RegisterFlag
15
+
16
+
17
+ class SequenceController(TableController):
18
+ """
19
+ Class containing sequence related logic
20
+ """
21
+
22
+ def __init__(self, controller: CommunicationController, src: str, data_dir: str):
23
+ super().__init__(controller, src, data_dir)
24
+
25
+ def switch(self, seq_name: str):
26
+ """
27
+ Switch to the specified sequence. The sequence name does not need the '.S' extension.
28
+
29
+ :param seq_name: The name of the sequence file
30
+ """
31
+ self.send(f'_SeqFile$ = "{seq_name}.S"')
32
+ self.send(f'_SeqPath$ = "{self.src}"')
33
+ self.send(Command.SWITCH_SEQUENCE_CMD)
34
+ time.sleep(2)
35
+ self.send(Command.GET_SEQUENCE_CMD)
36
+ time.sleep(2)
37
+ # check that method switched
38
+ for _ in range(10):
39
+ try:
40
+ parsed_response = self.receive().splitlines()[1].split()[1:][0]
41
+ break
42
+ except IndexError:
43
+ continue
44
+
45
+ assert parsed_response == f"{seq_name}.S", "Switching sequence failed."
46
+
47
+ def edit(self, sequence_table: SequenceTable):
48
+ """
49
+ Updates the currently loaded sequence table with the provided table. This method will delete the existing sequence table and remake it.
50
+ If you would only like to edit a single row of a sequence table, use `edit_sequence_table_row` instead.
51
+
52
+ :param sequence_table:
53
+ """
54
+ self.send("Local Rows")
55
+ self.sleep(1)
56
+ self.delete_table(SEQUENCE_TABLE)
57
+ self.sleep(1)
58
+ self.new_table(SEQUENCE_TABLE)
59
+ self.sleep(1)
60
+ self._get_table_rows(SEQUENCE_TABLE)
61
+
62
+ for _ in sequence_table.rows:
63
+ self.add_table_row(SEQUENCE_TABLE)
64
+ self.sleep(1)
65
+ self.send(Command.SAVE_SEQUENCE_CMD)
66
+ self._get_table_rows(SEQUENCE_TABLE)
67
+ self.send(Command.SAVE_SEQUENCE_CMD)
68
+ self.send(Command.SWITCH_SEQUENCE_CMD)
69
+
70
+ for i, row in enumerate(sequence_table.rows):
71
+ self.edit_row(row=row, row_num=i + 1)
72
+ self.sleep(1)
73
+ self.send(Command.SAVE_SEQUENCE_CMD)
74
+
75
+
76
+ def edit_row(self, row: SequenceEntry, row_num: int):
77
+ """
78
+ Edits a row in the sequence table. Assumes the row already exists.
79
+
80
+ :param row: sequence row entry with updated information
81
+ :param row_num: the row to edit, based on -1-based indexing
82
+ """
83
+ if row.vial_location:
84
+ self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(register=SEQUENCE_TABLE.register,
85
+ table_name=SEQUENCE_TABLE.name,
86
+ row=row_num,
87
+ col_name=RegisterFlag.VIAL_LOCATION,
88
+ val=row.vial_location))
89
+ if row.method:
90
+ self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(register=SEQUENCE_TABLE.register,
91
+ table_name=SEQUENCE_TABLE.name,
92
+ row=row_num,
93
+ col_name=RegisterFlag.METHOD,
94
+ val=row.method))
95
+
96
+ if row.num_inj:
97
+ self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(register=SEQUENCE_TABLE.register,
98
+ table_name=SEQUENCE_TABLE.name,
99
+ row=row_num,
100
+ col_name=RegisterFlag.NUM_INJ,
101
+ val=row.num_inj))
102
+
103
+ if row.inj_vol:
104
+ self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(register=SEQUENCE_TABLE.register,
105
+ table_name=SEQUENCE_TABLE.name,
106
+ row=row_num,
107
+ col_name=RegisterFlag.INJ_VOL,
108
+ val=row.inj_vol))
109
+
110
+ if row.inj_source:
111
+ self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(register=SEQUENCE_TABLE.register,
112
+ table_name=SEQUENCE_TABLE.name,
113
+ row=row_num,
114
+ col_name=RegisterFlag.INJ_SOR,
115
+ val=row.inj_source.value))
116
+
117
+ if row.sample_name:
118
+ self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(register=SEQUENCE_TABLE.register,
119
+ table_name=SEQUENCE_TABLE.name,
120
+ row=row_num,
121
+ col_name=RegisterFlag.NAME,
122
+ val=row.sample_name))
123
+ self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(register=SEQUENCE_TABLE.register,
124
+ table_name=SEQUENCE_TABLE.name,
125
+ row=row_num,
126
+ col_name=RegisterFlag.DATA_FILE,
127
+ val=row.sample_name))
128
+ if row.sample_type:
129
+ self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(register=SEQUENCE_TABLE.register,
130
+ table_name=SEQUENCE_TABLE.name,
131
+ row=row_num,
132
+ col_name=RegisterFlag.SAMPLE_TYPE,
133
+ val=row.sample_type.value))
134
+
135
+ def run(self, sequence_table: SequenceTable):
136
+ """
137
+ Starts the currently loaded sequence, storing data
138
+ under the <data_dir>/<sequence table name> folder.
139
+ Device must be ready.
140
+
141
+ :param sequence_table:
142
+ """
143
+ timestamp = time.strftime(SEQUENCE_TIME_FORMAT)
144
+ # self.send(Command.RUN_SEQUENCE_CMD.value)
145
+ folder_name = f"{sequence_table.name} {timestamp}"
146
+ subdirs = [x[0] for x in os.walk(self.data_dir)]
147
+ # time.sleep(60)
148
+ potential_folders = sorted(list(filter(lambda d: folder_name in d, subdirs)))
149
+ parent_folder = potential_folders[0]
150
+ self.data_files.append(SequenceDataFiles(
151
+ sequence_name=sequence_table.name,
152
+ dir=parent_folder,
153
+ child_dirs=[r.sample_name + ".D" for r in sequence_table.rows]))
154
+
155
+ def retrieve_recent_data_files(self):
156
+ sequence_data_files: SequenceDataFiles = self.data_files[-1]
157
+ return os.path.join(sequence_data_files.dir, sequence_data_files.child_dirs[-1])
158
+
159
+ def get_data(self) -> tuple[bool, Any]:
160
+ data_ready = True # self.data_ready()
161
+ sequence_data_files: SequenceDataFiles = self.data_files[-1]
162
+ spectra: list[dict[str, AgilentHPLCChromatogram]] = []
163
+ if data_ready:
164
+ for row in sequence_data_files.child_dirs:
165
+ data_path = os.path.join(sequence_data_files.dir, row)
166
+ self.get_spectrum(data_path)
167
+ spectra.append(deepcopy(self.spectra))
168
+ return data_ready, spectra
169
+ else:
170
+ return False, None
@@ -0,0 +1,137 @@
1
+ """
2
+ Abstract module containing shared logic for Method and Sequence tables.
3
+
4
+ Authors: Lucy Hao
5
+ """
6
+
7
+ import abc
8
+ import os
9
+ from typing import Union, Optional
10
+
11
+ import polling
12
+
13
+ from ...control import CommunicationController
14
+ from ...utils.chromatogram import AgilentHPLCChromatogram
15
+ from ...utils.macro import Command
16
+ from ...utils.method_types import MethodTimetable
17
+ from ...utils.sequence_types import SequenceDataFiles
18
+ from ...utils.table_types import Table, TableOperation, RegisterFlag
19
+
20
+
21
+ class TableController(abc.ABC):
22
+
23
+ def __init__(self, controller: CommunicationController, src: str, data_dir: str):
24
+ self.controller = controller
25
+ if os.path.isdir(src):
26
+ self.src: str = src
27
+ else:
28
+ raise FileNotFoundError(f"dir: {src} not found.")
29
+
30
+ if os.path.isdir(data_dir):
31
+ self.data_dir: str = data_dir
32
+ else:
33
+ raise FileNotFoundError(f"dir: {data_dir} not found.")
34
+
35
+ self.spectra = {
36
+ "A": AgilentHPLCChromatogram(self.data_dir),
37
+ "B": AgilentHPLCChromatogram(self.data_dir),
38
+ "C": AgilentHPLCChromatogram(self.data_dir),
39
+ "D": AgilentHPLCChromatogram(self.data_dir),
40
+ }
41
+
42
+ self.data_files: Union[list[SequenceDataFiles], list[str]] = []
43
+
44
+ def receive(self):
45
+ return self.controller.receive()
46
+
47
+ def send(self, cmd: Union[Command, str]):
48
+ self.controller.send(cmd)
49
+
50
+ def sleepy_send(self, cmd: Union[Command, str]):
51
+ self.controller.sleepy_send(cmd)
52
+
53
+ def sleep(self, seconds: int):
54
+ """
55
+ Tells the HPLC to wait for a specified number of seconds.
56
+
57
+ :param seconds: number of seconds to wait
58
+ """
59
+ self.send(Command.SLEEP_CMD.value.format(seconds=seconds))
60
+
61
+ def add_table_row(self, table: Table):
62
+ """
63
+ Adds a row to the provided table for currently loaded method or sequence.
64
+ Import either the SEQUENCE_TABLE or METHOD_TIMETABLE from hein_analytical_control.constants.
65
+ You can also provide your own table.
66
+
67
+ :param table: the table to add a new row to
68
+ """
69
+ self.sleepy_send(TableOperation.NEW_ROW.value.format(register=table.register,
70
+ table_name=table.name))
71
+
72
+ def delete_table(self, table: Table):
73
+ """
74
+ Deletes the table for the current loaded method or sequence.
75
+ Import either the SEQUENCE_TABLE or METHOD_TIMETABLE from hein_analytical_control.constants.
76
+ You can also provide your own table.
77
+
78
+ :param table: the table to delete
79
+ """
80
+ self.sleepy_send(TableOperation.DELETE_TABLE.value.format(register=table.register,
81
+ table_name=table.name))
82
+
83
+ def new_table(self, table: Table):
84
+ """
85
+ Creates the table for the currently loaded method or sequence. Import either the SEQUENCE_TABLE or
86
+ METHOD_TIMETABLE from hein_analytical_control.constants. You can also provide your own table.
87
+
88
+ :param table: the table to create
89
+ """
90
+ self.send(TableOperation.CREATE_TABLE.value.format(register=table.register,
91
+ table_name=table.name))
92
+
93
+ def _get_table_rows(self, table: Table) -> str:
94
+ self.send(TableOperation.GET_OBJ_HDR_VAL.value.format(internal_val="Rows",
95
+ register=table.register,
96
+ table_name=table.name,
97
+ col_name=RegisterFlag.NUM_ROWS, ))
98
+ res = self.controller.receive()
99
+ self.send("Sleep 1")
100
+ self.send('Print Rows')
101
+ return res
102
+
103
+ def check_hplc_ready_with_data(self, method: Optional[MethodTimetable] = None) -> bool:
104
+ """
105
+ Checks if ChemStation has finished writing data and can be read back.
106
+
107
+ :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
108
+ :return: Return True if data can be read back, else False.
109
+ """
110
+ self.controller.set_status()
111
+
112
+ timeout = 10 * 60
113
+ hplc_run_done = polling.poll(
114
+ lambda: self.controller.check_data(self.retrieve_recent_data_files()),
115
+ timeout=timeout,
116
+ step=30
117
+ )
118
+
119
+ return hplc_run_done
120
+
121
+ @abc.abstractmethod
122
+ def retrieve_recent_data_files(self):
123
+ pass
124
+
125
+ @abc.abstractmethod
126
+ def get_data(self) -> tuple[bool,]:
127
+ pass
128
+
129
+ def get_spectrum(self, data_file: str):
130
+ """
131
+ Load chromatogram for any channel in spectra dictionary.
132
+ """
133
+ for channel, spec in self.spectra.items():
134
+ spec.load_spectrum(data_path=data_file, channel=channel)
135
+
136
+ def data_ready(self) -> bool:
137
+ return self.check_hplc_ready_with_data()
@@ -12,14 +12,13 @@ from ..analysis import AbstractSpectrum
12
12
  # Chemstation data path
13
13
  DATA_DIR = r"C:\Chem32\1\Data"
14
14
 
15
-
16
15
  # standard filenames for spectral data
17
16
  CHANNELS = {"A": "01", "B": "02", "C": "03", "D": "04"}
18
17
 
19
18
  ACQUISITION_PARAMETERS = "acq.txt"
20
19
 
21
20
  # format used in acquisition parameters
22
- TIME_FORMAT = "%Y-%m-%d-%H-%M-%S"
21
+ TIME_FORMAT = "%Y-%m-%d %H-%M-%S"
23
22
  SEQUENCE_TIME_FORMAT = "%Y-%m-%d %H"
24
23
 
25
24
 
@@ -51,8 +50,6 @@ class AgilentHPLCChromatogram(AbstractSpectrum):
51
50
  self.path = os.path.join(".", "hplc_data")
52
51
  os.makedirs(self.path, exist_ok=True)
53
52
 
54
- self.logger = logging.getLogger("AgilentHPLCChromatogram")
55
-
56
53
  super().__init__(path=path, autosaving=autosaving)
57
54
 
58
55
  def load_spectrum(self, data_path, channel="A"):
@@ -73,7 +70,11 @@ class AgilentHPLCChromatogram(AbstractSpectrum):
73
70
 
74
71
  # get timestamp
75
72
  tstr = data_path.split(".")[0].split("_")[-1]
76
- timestamp = time.mktime(time.strptime(tstr, TIME_FORMAT))
73
+ timestamp = "NA"
74
+ try:
75
+ timestamp = time.mktime(time.strptime(tstr, TIME_FORMAT))
76
+ except ValueError:
77
+ pass
77
78
 
78
79
  # loading all data
79
80
  super().load_spectrum(x, y, timestamp)
@@ -26,6 +26,7 @@ class Command(Enum):
26
26
  UPDATE_METHOD_CMD = 'UpdateMethod'
27
27
  SWITCH_SEQUENCE_CMD = 'LoadSequence _SeqPath$, _SeqFile$'
28
28
  SAVE_SEQUENCE_CMD = 'SaveSequence _SeqPath$, _SeqFile$'
29
+ SAVE_METHOD_CMD = 'SaveMethod _MethPath$, _MethFile$, {commit_msg}'
29
30
  GET_SEQUENCE_CMD = 'response$ = _SeqFile$'
30
31
  RUN_SEQUENCE_CMD = 'RunSequence'
31
32
 
@@ -20,12 +20,10 @@ class Param:
20
20
 
21
21
  @dataclass
22
22
  class HPLCMethodParams:
23
- organic_modifier: Param
24
- flow: Param
25
- temperature: Param
26
- inj_vol: Param
27
- equ_time: Param
28
- maximum_run_time: Param
23
+ organic_modifier: int
24
+ flow: float
25
+ temperature: float
26
+ maximum_run_time: int
29
27
 
30
28
 
31
29
  @dataclass
@@ -3,27 +3,41 @@ from enum import Enum
3
3
  from typing import Optional
4
4
 
5
5
 
6
+ @dataclass
7
+ class TrayLocation:
8
+ row: str
9
+ col: int
10
+
11
+
12
+ @dataclass
13
+ class SequenceDataFiles:
14
+ sequence_name: str
15
+ dir: str
16
+ child_dirs: list[str]
17
+
18
+
6
19
  class SampleType(Enum):
7
20
  SAMPLE = 1
8
21
  BLANK = 2
9
- CONTROL = 3
10
- CALIBRATION = 4
22
+ CALIBRATION = 3
23
+ CONTROL = 4
11
24
 
12
25
 
13
26
  class InjectionSource(Enum):
14
- AS_METHOD = "AsMethod"
27
+ AS_METHOD = "As Method"
15
28
  MANUAL = "Manual"
29
+ MSD = "MSD"
16
30
  HIP_ALS = "HipAls"
17
31
 
18
32
 
19
33
  @dataclass
20
34
  class SequenceEntry:
21
- vial_location: Optional[int] = None
35
+ sample_name: str
36
+ vial_location: int
22
37
  method: Optional[str] = None
23
38
  num_inj: Optional[int] = 1
24
39
  inj_vol: Optional[int] = 2
25
40
  inj_source: Optional[InjectionSource] = InjectionSource.HIP_ALS
26
- sample_name: Optional[str] = None
27
41
  sample_type: Optional[SampleType] = SampleType.SAMPLE
28
42
 
29
43