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