pychemstation 0.5.11__py3-none-any.whl → 0.5.13__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 (29) hide show
  1. pychemstation/control/controllers/__init__.py +8 -3
  2. pychemstation/control/controllers/devices/__init__.py +0 -0
  3. pychemstation/control/controllers/devices/column.py +12 -0
  4. pychemstation/control/controllers/devices/device.py +23 -0
  5. pychemstation/control/controllers/devices/injector.py +11 -0
  6. pychemstation/control/controllers/devices/pump.py +43 -0
  7. pychemstation/control/controllers/table_controller.py +1 -1
  8. pychemstation/control/controllers/tables/__init__.py +0 -0
  9. pychemstation/control/controllers/tables/method.py +351 -0
  10. pychemstation/control/controllers/tables/ms.py +21 -0
  11. pychemstation/control/controllers/tables/sequence.py +200 -0
  12. pychemstation/control/controllers/tables/table.py +297 -0
  13. pychemstation/control/hplc.py +13 -7
  14. pychemstation/utils/injector_types.py +30 -0
  15. pychemstation/utils/macro.py +3 -1
  16. pychemstation/utils/method_types.py +3 -1
  17. pychemstation/utils/pump_types.py +7 -0
  18. pychemstation/utils/sequence_types.py +3 -2
  19. pychemstation/utils/table_types.py +2 -0
  20. pychemstation/utils/tray_types.py +6 -0
  21. {pychemstation-0.5.11.dist-info → pychemstation-0.5.13.dist-info}/METADATA +3 -2
  22. pychemstation-0.5.13.dist-info/RECORD +52 -0
  23. tests/constants.py +1 -1
  24. tests/test_inj.py +38 -0
  25. tests/test_method.py +21 -1
  26. pychemstation-0.5.11.dist-info/RECORD +0 -39
  27. {pychemstation-0.5.11.dist-info → pychemstation-0.5.13.dist-info}/LICENSE +0 -0
  28. {pychemstation-0.5.11.dist-info → pychemstation-0.5.13.dist-info}/WHEEL +0 -0
  29. {pychemstation-0.5.11.dist-info → pychemstation-0.5.13.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,200 @@
1
+ import os
2
+ import time
3
+ from typing import Optional, Union
4
+
5
+ from .table import TableController, ChromData
6
+ from ....control.controllers.comm import CommunicationController
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, FiftyFourVialPlate
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
+ offline: bool):
21
+ self.method_dir = method_dir
22
+ super().__init__(controller, src, data_dir, table, offline=offline)
23
+
24
+ def load(self) -> SequenceTable:
25
+ rows = self.get_num_rows()
26
+ self.send(Command.GET_SEQUENCE_CMD)
27
+ seq_name = self.receive()
28
+
29
+ if rows.is_ok() and seq_name.is_ok():
30
+ self.table_state = SequenceTable(
31
+ name=seq_name.ok_value.string_response.partition(".S")[0],
32
+ rows=[self.get_row(r + 1) for r in range(int(rows.ok_value.num_response))])
33
+ return self.table_state
34
+ raise RuntimeError(rows.err_value)
35
+
36
+ def get_row(self, row: int) -> SequenceEntry:
37
+ sample_name = self.get_text(row, RegisterFlag.NAME)
38
+ vial_location = int(self.get_num(row, RegisterFlag.VIAL_LOCATION))
39
+ method = self.get_text(row, RegisterFlag.METHOD)
40
+ num_inj = int(self.get_num(row, RegisterFlag.NUM_INJ))
41
+ inj_vol = int(self.get_text(row, RegisterFlag.INJ_VOL))
42
+ inj_source = InjectionSource(self.get_text(row, RegisterFlag.INJ_SOR))
43
+ sample_type = SampleType(self.get_num(row, RegisterFlag.SAMPLE_TYPE))
44
+ return SequenceEntry(sample_name=sample_name,
45
+ vial_location=vial_location,
46
+ method=None if len(method) == 0 else method,
47
+ num_inj=num_inj,
48
+ inj_vol=inj_vol,
49
+ inj_source=inj_source,
50
+ sample_type=sample_type)
51
+
52
+ def check(self) -> str:
53
+ time.sleep(2)
54
+ self.send(Command.GET_SEQUENCE_CMD)
55
+ time.sleep(2)
56
+ res = self.receive()
57
+ if res.is_ok():
58
+ return res.ok_value.string_response
59
+ return "ERROR"
60
+
61
+ def switch(self, seq_name: str):
62
+ """
63
+ Switch to the specified sequence. The sequence name does not need the '.S' extension.
64
+
65
+ :param seq_name: The name of the sequence file
66
+ """
67
+ self.send(f'_SeqFile$ = "{seq_name}.S"')
68
+ self.send(f'_SeqPath$ = "{self.src}"')
69
+ self.send(Command.SWITCH_SEQUENCE_CMD)
70
+ time.sleep(2)
71
+ self.send(Command.GET_SEQUENCE_CMD)
72
+ time.sleep(2)
73
+ parsed_response = self.receive().value.string_response
74
+
75
+ assert parsed_response == f"{seq_name}.S", "Switching sequence failed."
76
+ self.table_state = None
77
+
78
+ def edit(self, sequence_table: SequenceTable):
79
+ """
80
+ Updates the currently loaded sequence table with the provided table. This method will delete the existing sequence table and remake it.
81
+ If you would only like to edit a single row of a sequence table, use `edit_sequence_table_row` instead.
82
+
83
+ :param sequence_table:
84
+ """
85
+ self.table_state = sequence_table
86
+ rows = self.get_num_rows()
87
+ if rows.is_ok():
88
+ existing_row_num = rows.value.num_response
89
+ wanted_row_num = len(sequence_table.rows)
90
+ while existing_row_num != wanted_row_num:
91
+ if wanted_row_num > existing_row_num:
92
+ self.add_row()
93
+ elif wanted_row_num < existing_row_num:
94
+ self.delete_row(int(existing_row_num))
95
+ self.send(Command.SAVE_SEQUENCE_CMD)
96
+ existing_row_num = self.get_num_rows().ok_value.num_response
97
+ self.send(Command.SWITCH_SEQUENCE_CMD)
98
+
99
+ for i, row in enumerate(sequence_table.rows):
100
+ self.edit_row(row=row, row_num=i + 1)
101
+ self.sleep(1)
102
+ self.send(Command.SAVE_SEQUENCE_CMD)
103
+ self.send(Command.SWITCH_SEQUENCE_CMD)
104
+
105
+ def edit_row(self, row: SequenceEntry, row_num: int):
106
+ """
107
+ Edits a row in the sequence table. If a row does NOT exist, a new one will be created.
108
+
109
+ :param row: sequence row entry with updated information
110
+ :param row_num: the row to edit, based on 1-based indexing
111
+ """
112
+ num_rows = self.get_num_rows()
113
+ if num_rows.is_ok():
114
+ while num_rows.ok_value.num_response < row_num:
115
+ self.add_row()
116
+ self.send(Command.SAVE_SEQUENCE_CMD)
117
+ num_rows = self.get_num_rows()
118
+
119
+ if row.vial_location:
120
+ loc = row.vial_location
121
+ if isinstance(loc, TenVialColumn):
122
+ loc = row.vial_location.value
123
+ elif isinstance(loc, FiftyFourVialPlate):
124
+ loc = row.vial_location.value()
125
+ self.edit_row_num(row=row_num, col_name=RegisterFlag.VIAL_LOCATION, val=loc)
126
+
127
+ if row.method:
128
+ possible_path = os.path.join(self.method_dir, row.method) + ".M\\"
129
+ method = row.method
130
+ if os.path.exists(possible_path):
131
+ method = os.path.join(self.method_dir, row.method)
132
+ self.edit_row_text(row=row_num, col_name=RegisterFlag.METHOD, val=method)
133
+
134
+ if row.num_inj:
135
+ self.edit_row_num(row=row_num, col_name=RegisterFlag.NUM_INJ, val=row.num_inj)
136
+
137
+ if row.inj_vol:
138
+ self.edit_row_text(row=row_num, col_name=RegisterFlag.INJ_VOL, val=row.inj_vol)
139
+
140
+ if row.inj_source:
141
+ self.edit_row_text(row=row_num, col_name=RegisterFlag.INJ_SOR, val=row.inj_source.value)
142
+
143
+ if row.sample_name:
144
+ self.edit_row_text(row=row_num, col_name=RegisterFlag.NAME, val=row.sample_name)
145
+ if row.data_file:
146
+ self.edit_row_text(row=row_num, col_name=RegisterFlag.DATA_FILE, val=row.data_file)
147
+ else:
148
+ self.edit_row_text(row=row_num, col_name=RegisterFlag.DATA_FILE, val=row.sample_name)
149
+
150
+ if row.sample_type:
151
+ self.edit_row_num(row=row_num, col_name=RegisterFlag.SAMPLE_TYPE, val=row.sample_type.value)
152
+
153
+ self.send(Command.SAVE_SEQUENCE_CMD)
154
+
155
+ def run(self, stall_while_running: bool = True):
156
+ """
157
+ Starts the currently loaded sequence, storing data
158
+ under the <data_dir>/<sequence table name> folder.
159
+ Device must be ready.
160
+ """
161
+ if not self.table_state:
162
+ self.table_state = self.load()
163
+
164
+ timestamp = time.strftime(SEQUENCE_TIME_FORMAT)
165
+ self.send(Command.RUN_SEQUENCE_CMD.value)
166
+
167
+ if self.check_hplc_is_running():
168
+ folder_name = f"{self.table_state.name} {timestamp}"
169
+ self.data_files.append(SequenceDataFiles(dir=folder_name,
170
+ sequence_name=self.table_state.name))
171
+
172
+ if stall_while_running:
173
+ run_completed = self.check_hplc_done_running(sequence=self.table_state)
174
+ if run_completed.is_ok():
175
+ self.data_files[-1].dir = run_completed.value
176
+ else:
177
+ raise RuntimeError("Run error has occurred.")
178
+ else:
179
+ self.data_files[-1].dir = self.fuzzy_match_most_recent_folder(folder_name).ok_value
180
+
181
+ def retrieve_recent_data_files(self) -> str:
182
+ sequence_data_files: SequenceDataFiles = self.data_files[-1]
183
+ return sequence_data_files.dir
184
+
185
+ def get_data(self, custom_path: Optional[str] = None,
186
+ read_uv: bool = False) -> list[AgilentChannelChromatogramData]:
187
+ parent_dir = self.data_files[-1].dir if not custom_path else custom_path
188
+ subdirs = [x[0] for x in os.walk(self.data_dir)]
189
+ potential_folders = sorted(list(filter(lambda d: parent_dir in d, subdirs)))
190
+ self.data_files[-1].child_dirs = [f for f in potential_folders if
191
+ parent_dir in f and ".M" not in f and ".D" in f]
192
+
193
+ spectra: list[Union[AgilentChannelChromatogramData, ChromData]] = []
194
+ all_w_spectra: list[Union[AgilentChannelChromatogramData, ChromData]] = []
195
+ for row in self.data_files[-1].child_dirs:
196
+ self.get_spectrum(row, read_uv)
197
+ spectra.append(AgilentChannelChromatogramData(**self.spectra))
198
+ all_w_spectra.append(self.uv)
199
+
200
+ return spectra if not read_uv else all_w_spectra
@@ -0,0 +1,297 @@
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 dataclasses import dataclass
10
+ from typing import Union, Optional
11
+
12
+ import numpy as np
13
+ import polling
14
+ import rainbow as rb
15
+ from result import Result, Ok, Err
16
+
17
+ from ....control.controllers.comm import CommunicationController
18
+ from ....utils.chromatogram import AgilentHPLCChromatogram, AgilentChannelChromatogramData
19
+ from ....utils.macro import Command, HPLCRunningStatus, Response
20
+ from ....utils.method_types import MethodDetails
21
+ from ....utils.sequence_types import SequenceDataFiles, SequenceTable
22
+ from ....utils.table_types import Table, TableOperation, RegisterFlag
23
+
24
+ TableType = Union[MethodDetails, SequenceTable]
25
+
26
+
27
+ @dataclass
28
+ class ChromData:
29
+ x: np.array
30
+ y: np.array
31
+
32
+
33
+ class TableController(abc.ABC):
34
+
35
+ def __init__(self, controller: CommunicationController,
36
+ src: str,
37
+ data_dir: str,
38
+ table: Table,
39
+ offline: bool = False):
40
+ self.controller = controller
41
+ self.table = table
42
+ self.table_state: Optional[TableType] = None
43
+
44
+ if not offline:
45
+ if os.path.isdir(src):
46
+ self.src: str = src
47
+ else:
48
+ raise FileNotFoundError(f"dir: {src} not found.")
49
+
50
+ if os.path.isdir(data_dir):
51
+ self.data_dir: str = data_dir
52
+ else:
53
+ raise FileNotFoundError(f"dir: {data_dir} not found.")
54
+
55
+ self.spectra: dict[str, Optional[AgilentHPLCChromatogram]] = {
56
+ "A": AgilentHPLCChromatogram(self.data_dir),
57
+ "B": AgilentHPLCChromatogram(self.data_dir),
58
+ "C": AgilentHPLCChromatogram(self.data_dir),
59
+ "D": AgilentHPLCChromatogram(self.data_dir),
60
+ "E": AgilentHPLCChromatogram(self.data_dir),
61
+ "F": AgilentHPLCChromatogram(self.data_dir),
62
+ "G": AgilentHPLCChromatogram(self.data_dir),
63
+ "H": AgilentHPLCChromatogram(self.data_dir),
64
+ }
65
+
66
+ self.data_files: Union[list[SequenceDataFiles], list[str]] = []
67
+
68
+ self.uv = None
69
+
70
+ # Initialize row counter for table operations
71
+ self.send('Local Rows')
72
+
73
+ def receive(self) -> Result[Response, str]:
74
+ for _ in range(10):
75
+ try:
76
+ return self.controller.receive()
77
+ except IndexError:
78
+ continue
79
+ return Err("Could not parse response")
80
+
81
+ def send(self, cmd: Union[Command, str]):
82
+ if not self.controller:
83
+ raise RuntimeError(
84
+ "Communication controller must be initialized before sending command. It is currently in offline mode.")
85
+ self.controller.send(cmd)
86
+
87
+ def sleepy_send(self, cmd: Union[Command, str]):
88
+ self.controller.sleepy_send(cmd)
89
+
90
+ def sleep(self, seconds: int):
91
+ """
92
+ Tells the HPLC to wait for a specified number of seconds.
93
+
94
+ :param seconds: number of seconds to wait
95
+ """
96
+ self.send(Command.SLEEP_CMD.value.format(seconds=seconds))
97
+
98
+ def get_num(self, row: int, col_name: RegisterFlag) -> float:
99
+ return self.controller.get_num_val(TableOperation.GET_ROW_VAL.value.format(register=self.table.register,
100
+ table_name=self.table.name,
101
+ row=row,
102
+ col_name=col_name.value))
103
+
104
+ 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))
109
+
110
+ def add_new_col_num(self,
111
+ col_name: RegisterFlag,
112
+ val: Union[int, float]):
113
+ self.sleepy_send(TableOperation.NEW_COL_VAL.value.format(
114
+ register=self.table.register,
115
+ table_name=self.table.name,
116
+ col_name=col_name,
117
+ val=val))
118
+
119
+ def add_new_col_text(self,
120
+ col_name: RegisterFlag,
121
+ val: str):
122
+ self.sleepy_send(TableOperation.NEW_COL_TEXT.value.format(
123
+ register=self.table.register,
124
+ table_name=self.table.name,
125
+ col_name=col_name,
126
+ val=val))
127
+
128
+ def edit_row_num(self,
129
+ col_name: RegisterFlag,
130
+ val: Union[int, float],
131
+ row: Optional[int] = None):
132
+ self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(
133
+ register=self.table.register,
134
+ table_name=self.table.name,
135
+ row=row if row is not None else 'Rows',
136
+ col_name=col_name,
137
+ val=val))
138
+
139
+ def edit_row_text(self,
140
+ col_name: RegisterFlag,
141
+ val: str,
142
+ row: Optional[int] = None):
143
+ self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(
144
+ register=self.table.register,
145
+ table_name=self.table.name,
146
+ row=row if row is not None else 'Rows',
147
+ col_name=col_name,
148
+ val=val))
149
+
150
+ @abc.abstractmethod
151
+ def get_row(self, row: int):
152
+ pass
153
+
154
+ 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,
157
+ row=row))
158
+
159
+ def add_row(self):
160
+ """
161
+ 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
+ """
167
+ self.sleepy_send(TableOperation.NEW_ROW.value.format(register=self.table.register,
168
+ table_name=self.table.name))
169
+
170
+ def delete_table(self):
171
+ """
172
+ 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
+ """
178
+ self.sleepy_send(TableOperation.DELETE_TABLE.value.format(register=self.table.register,
179
+ table_name=self.table.name))
180
+
181
+ def new_table(self):
182
+ """
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
187
+ """
188
+ self.send(TableOperation.CREATE_TABLE.value.format(register=self.table.register,
189
+ table_name=self.table.name))
190
+
191
+ 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,
194
+ col_name=RegisterFlag.NUM_ROWS))
195
+ self.send(Command.GET_ROWS_CMD.value.format(register=self.table.register,
196
+ table_name=self.table.name,
197
+ col_name=RegisterFlag.NUM_ROWS))
198
+ res = self.controller.receive()
199
+
200
+ if res.is_ok():
201
+ self.send("Sleep 0.1")
202
+ self.send('Print Rows')
203
+ return res
204
+ else:
205
+ return Err("No rows could be read.")
206
+
207
+ def check_hplc_is_running(self) -> bool:
208
+ try:
209
+ started_running = polling.poll(lambda: isinstance(self.controller.get_status(), HPLCRunningStatus),
210
+ step=3, max_tries=10)
211
+ except Exception as e:
212
+ print(e)
213
+ return False
214
+ return started_running
215
+
216
+ def check_hplc_done_running(self,
217
+ method: Optional[MethodDetails] = None,
218
+ sequence: Optional[SequenceTable] = None) -> Result[str, str]:
219
+ """
220
+ Checks if ChemStation has finished running and can read data back
221
+
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
+ :return: Return True if data can be read back, else False.
225
+ """
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
+ finished_run = False
235
+ 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)
244
+ if check_folder.is_ok() and finished_run:
245
+ return check_folder
246
+ 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:
252
+ return check_folder
253
+ return check_folder
254
+ else:
255
+ return Err("Run did not complete as expected")
256
+
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 = [x[0] for x in os.walk(self.data_dir)]
262
+ potential_folders = sorted(list(filter(lambda d: most_recent_folder in d, subdirs)))
263
+ parent_dirs = []
264
+ for folder in potential_folders:
265
+ path = os.path.normpath(folder)
266
+ split_folder = path.split(os.sep)
267
+ if most_recent_folder in split_folder[-1]:
268
+ parent_dirs.append(folder)
269
+ parent_dir = sorted(parent_dirs, reverse=True)[0]
270
+ return Ok(parent_dir)
271
+
272
+ @abc.abstractmethod
273
+ def retrieve_recent_data_files(self):
274
+ pass
275
+
276
+ @abc.abstractmethod
277
+ def get_data(self) -> Union[list[AgilentChannelChromatogramData], AgilentChannelChromatogramData]:
278
+ pass
279
+
280
+ def get_uv_spectrum(self, path: str):
281
+ data_uv = rb.agilent.chemstation.parse_file(os.path.join(path, "DAD1.UV"))
282
+ zipped_data = zip(data_uv.ylabels, data_uv.data)
283
+ self.uv = {str(w_a[0]): ChromData(x=data_uv.xlabels, y=w_a[1]) for w_a in zipped_data}
284
+
285
+ def get_spectrum(self, data_path: str, read_uv: bool = False):
286
+ """
287
+ Load chromatogram for any channel in spectra dictionary.
288
+ """
289
+ if read_uv:
290
+ self.get_uv_spectrum(data_path)
291
+
292
+ for channel, spec in self.spectra.items():
293
+ try:
294
+ spec.load_spectrum(data_path=data_path, channel=channel)
295
+ except FileNotFoundError:
296
+ self.spectra[channel] = None
297
+ print(f"No data at channel: {channel}")
@@ -41,18 +41,20 @@ class HPLCController:
41
41
  `comm_dir` must match the file path in the macro file.
42
42
 
43
43
  :param comm_dir: Name of directory for communication, where ChemStation will read and write from. Can be any existing directory.
44
- :raises FileNotFoundError: If either `data_dir`, `method_dir` or `comm_dir` is not a valid directory.
44
+ :raises FileNotFoundError: If either `data_dir`, `method_dir`, `sequence_dir`, or `comm_dir` is not a valid directory.
45
45
  """
46
46
  self.comm = CommunicationController(comm_dir=comm_dir) if not offline else None
47
47
  self.method_controller = MethodController(controller=self.comm,
48
48
  src=method_dir,
49
49
  data_dir=data_dir,
50
- table=self.METHOD_TIMETABLE)
50
+ table=self.METHOD_TIMETABLE,
51
+ offline=offline)
51
52
  self.sequence_controller = SequenceController(controller=self.comm,
52
53
  src=sequence_dir,
53
54
  data_dir=data_dir,
54
55
  table=self.SEQUENCE_TABLE,
55
- method_dir=method_dir)
56
+ method_dir=method_dir,
57
+ offline=offline)
56
58
 
57
59
  def send(self, cmd: Union[Command, str]):
58
60
  if not self.comm:
@@ -142,21 +144,25 @@ class HPLCController:
142
144
  """
143
145
  self.sequence_controller.edit_row(row, num)
144
146
 
145
- def get_last_run_method_data(self, data: Optional[str] = None) -> AgilentChannelChromatogramData:
147
+ def get_last_run_method_data(self, read_uv: bool = False,
148
+ data: Optional[str] = None) -> AgilentChannelChromatogramData:
146
149
  """
147
150
  Returns the last run method data.
148
151
 
149
152
  :param data: If you want to just load method data but from a file path. This file path must be the complete file path.
153
+ :param read_uv: whether to also read the UV file
150
154
  """
151
- return self.method_controller.get_data(custom_path=data)
155
+ return self.method_controller.get_data(custom_path=data, read_uv=read_uv)
152
156
 
153
- def get_last_run_sequence_data(self, data: Optional[str] = None) -> list[AgilentChannelChromatogramData]:
157
+ def get_last_run_sequence_data(self, read_uv: bool = False,
158
+ data: Optional[str] = None) -> list[AgilentChannelChromatogramData]:
154
159
  """
155
160
  Returns data for all rows in the last run sequence data.
156
161
 
157
162
  :param data: If you want to just load sequence data but from a file path. This file path must be the complete file path.
163
+ :param read_uv: whether to also read the UV file
158
164
  """
159
- return self.sequence_controller.get_data(custom_path=data)
165
+ return self.sequence_controller.get_data(custom_path=data, read_uv=read_uv)
160
166
 
161
167
  def check_loaded_sequence(self) -> str:
162
168
  """
@@ -0,0 +1,30 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any
3
+
4
+ from pychemstation.utils.tray_types import Tray
5
+
6
+
7
+ @dataclass
8
+ class Draw:
9
+ amount: float
10
+ source: Tray
11
+ speed: Any
12
+ offset: Any
13
+
14
+
15
+ @dataclass
16
+ class Wait:
17
+ time: int
18
+
19
+
20
+ @dataclass
21
+ class Inject:
22
+ pass
23
+
24
+
25
+ InjectorFunction = [Draw, Wait, Inject]
26
+
27
+
28
+ @dataclass
29
+ class InjectorTable:
30
+ functions: list[InjectorFunction]
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from enum import Enum
2
4
  from typing import Union
3
5
  from dataclasses import dataclass
@@ -32,7 +34,7 @@ class Command(Enum):
32
34
  INSTRUMENT_OFF = 'macro "SHUTDOWN.MAC" ,go'
33
35
  INSTRUMENT_ON = 'LIDoOperation "TURN_ON"'
34
36
 
35
-
37
+ # Method and Sequence Related
36
38
  GET_METHOD_CMD = "response$ = _MethFile$"
37
39
  GET_ROWS_CMD = 'response_num = TabHdrVal({register}, "{table_name}", "{col_name}")'
38
40
  SWITCH_METHOD_CMD = 'LoadMethod "{method_dir}", "{method_name}.M"'
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from dataclasses import dataclass
2
4
  from enum import Enum
3
5
  from typing import Union, Any, Optional
@@ -22,7 +24,7 @@ class Param:
22
24
  class HPLCMethodParams:
23
25
  organic_modifier: int
24
26
  flow: float
25
- pressure: Optional[float] = None #TODO: find this
27
+ pressure: Optional[float] = None # TODO: find this
26
28
 
27
29
 
28
30
  @dataclass
@@ -0,0 +1,7 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class Pump:
6
+ solvent: str
7
+ in_use: bool
@@ -4,7 +4,7 @@ from dataclasses import dataclass
4
4
  from enum import Enum
5
5
  from typing import Optional, Union
6
6
 
7
- from pychemstation.utils.tray_types import TenVialColumn
7
+ from pychemstation.utils.tray_types import TenVialColumn, Tray
8
8
 
9
9
 
10
10
  @dataclass
@@ -39,7 +39,8 @@ class InjectionSource(Enum):
39
39
  @dataclass
40
40
  class SequenceEntry:
41
41
  sample_name: str
42
- vial_location: Union[TenVialColumn, int]
42
+ vial_location: Tray
43
+ data_file: Optional[str] = None
43
44
  method: Optional[str] = None
44
45
  num_inj: Optional[int] = 1
45
46
  inj_vol: Optional[int] = 2
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from dataclasses import dataclass
2
4
  from enum import Enum
3
5
 
@@ -1,5 +1,8 @@
1
+ from __future__ import annotations
2
+
1
3
  from dataclasses import dataclass
2
4
  from enum import Enum
5
+ from typing import Union
3
6
 
4
7
 
5
8
  class Num(Enum):
@@ -48,3 +51,6 @@ class TenVialColumn(Enum):
48
51
  EIGHT = 8
49
52
  NINE = 9
50
53
  TEN = 10
54
+
55
+
56
+ Tray = Union[FiftyFourVialPlate, TenVialColumn]