pychemstation 0.4.7.dev1__py3-none-any.whl → 0.4.7.dev2__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 (104) hide show
  1. pychemstation/control/__init__.py +3 -2
  2. hein-analytical-control/devices/Agilent/hplc.py → pychemstation/control/comm.py +21 -181
  3. pychemstation/control/method.py +232 -0
  4. pychemstation/control/sequence.py +140 -0
  5. pychemstation/control/table_controller.py +75 -0
  6. pychemstation/utils/__init__.py +0 -2
  7. {ag_hplc_macro/control → pychemstation/utils}/chromatogram.py +2 -1
  8. pychemstation/utils/constants.py +1 -1
  9. hein_analytical_control/devices/Agilent/hplc_param_types.py → pychemstation/utils/macro.py +5 -69
  10. pychemstation/utils/method_types.py +44 -0
  11. pychemstation/utils/sequence_types.py +33 -0
  12. pychemstation/utils/table_types.py +60 -0
  13. {pychemstation-0.4.7.dev1.dist-info → pychemstation-0.4.7.dev2.dist-info}/METADATA +13 -12
  14. pychemstation-0.4.7.dev2.dist-info/RECORD +30 -0
  15. ag_hplc_macro/__init__.py +0 -3
  16. ag_hplc_macro/analysis/__init__.py +0 -1
  17. ag_hplc_macro/analysis/base_spectrum.py +0 -509
  18. ag_hplc_macro/analysis/spec_utils.py +0 -304
  19. ag_hplc_macro/analysis/utils.py +0 -63
  20. ag_hplc_macro/control/__init__.py +0 -5
  21. ag_hplc_macro/control/hplc.py +0 -673
  22. ag_hplc_macro/generated/__init__.py +0 -56
  23. ag_hplc_macro/generated/dad_method.py +0 -367
  24. ag_hplc_macro/generated/pump_method.py +0 -519
  25. ag_hplc_macro/utils/__init__.py +0 -2
  26. ag_hplc_macro/utils/constants.py +0 -15
  27. ag_hplc_macro/utils/hplc_param_types.py +0 -185
  28. hein-analytical-control/__init__.py +0 -3
  29. hein-analytical-control/analysis/__init__.py +0 -1
  30. hein-analytical-control/analysis/base_spectrum.py +0 -509
  31. hein-analytical-control/analysis/spec_utils.py +0 -304
  32. hein-analytical-control/analysis/utils.py +0 -63
  33. hein-analytical-control/devices/Agilent/__init__.py +0 -3
  34. hein-analytical-control/devices/Agilent/chemstation.py +0 -290
  35. hein-analytical-control/devices/Agilent/chromatogram.py +0 -129
  36. hein-analytical-control/devices/Agilent/hplc_param_types.py +0 -141
  37. hein-analytical-control/devices/Magritek/Spinsolve/__init__.py +0 -0
  38. hein-analytical-control/devices/Magritek/Spinsolve/commands.py +0 -495
  39. hein-analytical-control/devices/Magritek/Spinsolve/spectrum.py +0 -822
  40. hein-analytical-control/devices/Magritek/Spinsolve/spinsolve.py +0 -425
  41. hein-analytical-control/devices/Magritek/Spinsolve/utils/__init__.py +0 -5
  42. hein-analytical-control/devices/Magritek/Spinsolve/utils/connection.py +0 -168
  43. hein-analytical-control/devices/Magritek/Spinsolve/utils/constants.py +0 -8
  44. hein-analytical-control/devices/Magritek/Spinsolve/utils/exceptions.py +0 -25
  45. hein-analytical-control/devices/Magritek/Spinsolve/utils/parser.py +0 -340
  46. hein-analytical-control/devices/Magritek/Spinsolve/utils/shimming.py +0 -55
  47. hein-analytical-control/devices/Magritek/Spinsolve/utils/spinsolve_logging.py +0 -43
  48. hein-analytical-control/devices/Magritek/__init__.py +0 -0
  49. hein-analytical-control/devices/OceanOptics/IR/NIRQuest512.py +0 -90
  50. hein-analytical-control/devices/OceanOptics/IR/__init__.py +0 -0
  51. hein-analytical-control/devices/OceanOptics/IR/ir_spectrum.py +0 -191
  52. hein-analytical-control/devices/OceanOptics/Raman/__init__.py +0 -0
  53. hein-analytical-control/devices/OceanOptics/Raman/raman_control.py +0 -46
  54. hein-analytical-control/devices/OceanOptics/Raman/raman_spectrum.py +0 -148
  55. hein-analytical-control/devices/OceanOptics/UV/QEPro2192.py +0 -90
  56. hein-analytical-control/devices/OceanOptics/UV/__init__.py +0 -0
  57. hein-analytical-control/devices/OceanOptics/UV/uv_spectrum.py +0 -227
  58. hein-analytical-control/devices/OceanOptics/__init__.py +0 -0
  59. hein-analytical-control/devices/OceanOptics/oceanoptics.py +0 -115
  60. hein-analytical-control/devices/__init__.py +0 -15
  61. hein-analytical-control/generated/__init__.py +0 -56
  62. hein-analytical-control/generated/dad_method.py +0 -367
  63. hein-analytical-control/generated/pump_method.py +0 -519
  64. hein_analytical_control/__init__.py +0 -3
  65. hein_analytical_control/analysis/__init__.py +0 -1
  66. hein_analytical_control/analysis/base_spectrum.py +0 -509
  67. hein_analytical_control/analysis/spec_utils.py +0 -304
  68. hein_analytical_control/analysis/utils.py +0 -63
  69. hein_analytical_control/devices/Agilent/__init__.py +0 -3
  70. hein_analytical_control/devices/Agilent/chemstation.py +0 -290
  71. hein_analytical_control/devices/Agilent/chromatogram.py +0 -129
  72. hein_analytical_control/devices/Agilent/hplc.py +0 -436
  73. hein_analytical_control/devices/Magritek/Spinsolve/__init__.py +0 -0
  74. hein_analytical_control/devices/Magritek/Spinsolve/commands.py +0 -495
  75. hein_analytical_control/devices/Magritek/Spinsolve/spectrum.py +0 -822
  76. hein_analytical_control/devices/Magritek/Spinsolve/spinsolve.py +0 -425
  77. hein_analytical_control/devices/Magritek/Spinsolve/utils/__init__.py +0 -5
  78. hein_analytical_control/devices/Magritek/Spinsolve/utils/connection.py +0 -168
  79. hein_analytical_control/devices/Magritek/Spinsolve/utils/constants.py +0 -8
  80. hein_analytical_control/devices/Magritek/Spinsolve/utils/exceptions.py +0 -25
  81. hein_analytical_control/devices/Magritek/Spinsolve/utils/parser.py +0 -340
  82. hein_analytical_control/devices/Magritek/Spinsolve/utils/shimming.py +0 -55
  83. hein_analytical_control/devices/Magritek/Spinsolve/utils/spinsolve_logging.py +0 -43
  84. hein_analytical_control/devices/Magritek/__init__.py +0 -0
  85. hein_analytical_control/devices/OceanOptics/IR/NIRQuest512.py +0 -90
  86. hein_analytical_control/devices/OceanOptics/IR/__init__.py +0 -0
  87. hein_analytical_control/devices/OceanOptics/IR/ir_spectrum.py +0 -191
  88. hein_analytical_control/devices/OceanOptics/Raman/__init__.py +0 -0
  89. hein_analytical_control/devices/OceanOptics/Raman/raman_control.py +0 -46
  90. hein_analytical_control/devices/OceanOptics/Raman/raman_spectrum.py +0 -148
  91. hein_analytical_control/devices/OceanOptics/UV/QEPro2192.py +0 -90
  92. hein_analytical_control/devices/OceanOptics/UV/__init__.py +0 -0
  93. hein_analytical_control/devices/OceanOptics/UV/uv_spectrum.py +0 -227
  94. hein_analytical_control/devices/OceanOptics/__init__.py +0 -0
  95. hein_analytical_control/devices/OceanOptics/oceanoptics.py +0 -115
  96. hein_analytical_control/devices/__init__.py +0 -15
  97. hein_analytical_control/generated/__init__.py +0 -56
  98. hein_analytical_control/generated/dad_method.py +0 -367
  99. hein_analytical_control/generated/pump_method.py +0 -519
  100. pychemstation-0.4.7.dev1.dist-info/RECORD +0 -109
  101. /ag_hplc_macro/utils/chemstation.py → /pychemstation/utils/parsing.py +0 -0
  102. {pychemstation-0.4.7.dev1.dist-info → pychemstation-0.4.7.dev2.dist-info}/LICENSE +0 -0
  103. {pychemstation-0.4.7.dev1.dist-info → pychemstation-0.4.7.dev2.dist-info}/WHEEL +0 -0
  104. {pychemstation-0.4.7.dev1.dist-info → pychemstation-0.4.7.dev2.dist-info}/top_level.txt +0 -0
@@ -1,673 +0,0 @@
1
- """
2
- Module to provide API for the remote control of the Agilent HPLC systems.
3
-
4
- HPLCController sends commands to Chemstation software via a command file.
5
- Answers are received via reply file. On the Chemstation side, a custom
6
- Macro monitors the command file, executes commands and writes to the reply file.
7
- Each command is given a number (cmd_no) to keep track of which commands have
8
- been processed.
9
-
10
- Authors: Alexander Hammer, Hessam Mehr, Lucy Hao
11
- """
12
-
13
- import logging
14
- import os
15
- import time
16
- from typing import Union
17
-
18
- import polling
19
- from xsdata.formats.dataclass.parsers import XmlParser
20
-
21
- from .chromatogram import AgilentHPLCChromatogram, TIME_FORMAT
22
- from ..generated import PumpMethod, DadMethod
23
- from ..utils.constants import MAX_CMD_NO, SEQUENCE_TABLE, METHOD_TIMETABLE
24
- from ..utils.hplc_param_types import HPLCAvailStatus, Command, HPLCErrorStatus, Table, TableOperation, SequenceTable, \
25
- SequenceEntry, RegisterFlag, MethodTimetable, Param, PType, str_to_status, HPLCRunningStatus, Entry, \
26
- HPLCMethodParams
27
-
28
-
29
- class HPLCController:
30
- """
31
- Class to control Agilent HPLC systems via Chemstation Macros.
32
- """
33
-
34
- def __init__(
35
- self,
36
- comm_dir: str,
37
- data_dir: str,
38
- method_dir: str,
39
- sequence_dir: str,
40
- cmd_file: str = "cmd",
41
- reply_file: str = "reply",
42
- ):
43
- """Initialize HPLC controller. The `hplc_talk.mac` macro file must be loaded in the Chemstation software.
44
- `comm_dir` must match the file path in the macro file.
45
-
46
- :param comm_dir: Name of directory for communication, where ChemStation will read and write from. Can be any existing directory.
47
- :param data_dir: Name of directory that ChemStation saves run data. Must be accessible by ChemStation.
48
- :param cmd_file: Name of command file
49
- :param reply_file: Name of reply file
50
- :raises FileNotFoundError: If either `data_dir`, `method_dir` or `comm_dir` is not a valid directory.
51
- """
52
- if os.path.isdir(comm_dir):
53
- self.cmd_file = os.path.join(comm_dir, cmd_file)
54
- self.reply_file = os.path.join(comm_dir, reply_file)
55
- self.cmd_no = 0
56
- else:
57
- raise FileNotFoundError(f"comm_dir: {comm_dir} not found.")
58
- self._most_recent_hplc_status = None
59
-
60
- if os.path.isdir(data_dir):
61
- self.data_dir = data_dir
62
- else:
63
- raise FileNotFoundError(f"data_dir: {data_dir} not found.")
64
-
65
- if os.path.isdir(method_dir):
66
- self.method_dir = method_dir
67
- else:
68
- raise FileNotFoundError(f"method_dir: {method_dir} not found.")
69
-
70
- if os.path.isdir(sequence_dir):
71
- self.sequence_dir = sequence_dir
72
- else:
73
- raise FileNotFoundError(f"method_dir: {method_dir} not found.")
74
-
75
- self.spectra = {
76
- "A": AgilentHPLCChromatogram(self.data_dir),
77
- "B": AgilentHPLCChromatogram(self.data_dir),
78
- "C": AgilentHPLCChromatogram(self.data_dir),
79
- "D": AgilentHPLCChromatogram(self.data_dir),
80
- }
81
-
82
- self.data_files: list[str] = []
83
- self.internal_variables: list[dict[str, str]] = []
84
-
85
- # Create files for Chemstation to communicate with Python
86
- open(self.cmd_file, "a").close()
87
- open(self.reply_file, "a").close()
88
-
89
- self.logger = logging.getLogger("hplc_logger")
90
- self.logger.addHandler(logging.NullHandler())
91
-
92
- self.reset_cmd_counter()
93
-
94
- self.logger.info("HPLC Controller initialized.")
95
-
96
- def _set_status(self):
97
- """Updates current status of HPLC machine"""
98
- self._most_recent_hplc_status = self.status()[0]
99
-
100
- def _check_data_status(self) -> bool:
101
- """Checks if HPLC machine is in an available state, meaning a state that data is not being written.
102
-
103
- :return: whether the HPLC machine is in a safe state to retrieve data back."""
104
- old_status = self._most_recent_hplc_status
105
- self._set_status()
106
- file_exists = os.path.exists(self.data_files[-1]) if len(self.data_files) > 0 else False
107
- done_writing_data = isinstance(self._most_recent_hplc_status,
108
- HPLCAvailStatus) and old_status != self._most_recent_hplc_status and file_exists
109
- return done_writing_data
110
-
111
- def check_hplc_ready_with_data(self) -> bool:
112
- """Checks if ChemStation has finished writing data and can be read back.
113
-
114
- :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
115
-
116
- :return: Return True if data can be read back, else False.
117
- """
118
- self._set_status()
119
-
120
- timeout = 10 * 60
121
- hplc_run_done = polling.poll(
122
- lambda: self._check_data_status(),
123
- timeout=timeout,
124
- step=30
125
- )
126
-
127
- return hplc_run_done
128
-
129
- def _send(self, cmd: str, cmd_no: int, num_attempts=5) -> None:
130
- """Low-level execution primitive. Sends a command string to HPLC.
131
-
132
- :param cmd: string to be sent to HPLC
133
- :param cmd_no: Command number
134
- :param num_attempts: Number of attempts to send the command before raising exception.
135
- :raises IOError: Could not write to command file.
136
- """
137
- err = None
138
- for _ in range(num_attempts):
139
- time.sleep(1)
140
- try:
141
- with open(self.cmd_file, "w", encoding="utf8") as cmd_file:
142
- cmd_file.write(f"{cmd_no} {cmd}")
143
- except IOError as e:
144
- err = e
145
- self.logger.warning("Failed to send command; trying again.")
146
- continue
147
- else:
148
- self.logger.info("Sent command #%d: %s.", cmd_no, cmd)
149
- return
150
- else:
151
- raise IOError(f"Failed to send command #{cmd_no}: {cmd}.") from err
152
-
153
- def _receive(self, cmd_no: int, num_attempts=100) -> str:
154
- """Low-level execution primitive. Recives a response from HPLC.
155
-
156
- :param cmd_no: Command number
157
- :param num_attempts: Number of retries to open reply file
158
- :raises IOError: Could not read reply file.
159
- :return: ChemStation response
160
- """
161
- err = None
162
- for _ in range(num_attempts):
163
- time.sleep(1)
164
-
165
- try:
166
- with open(self.reply_file, "r", encoding="utf_16") as reply_file:
167
- response = reply_file.read()
168
- except OSError as e:
169
- err = e
170
- self.logger.warning("Failed to read from reply file; trying again.")
171
- continue
172
-
173
- try:
174
- first_line = response.splitlines()[0]
175
- response_no = int(first_line.split()[0])
176
- except IndexError as e:
177
- err = e
178
- self.logger.warning("Malformed response %s; trying again.", response)
179
- continue
180
-
181
- # check that response corresponds to sent command
182
- if response_no == cmd_no:
183
- self.logger.info("Reply: \n%s", response)
184
- return response
185
- else:
186
- self.logger.warning(
187
- "Response #: %d != command #: %d; trying again.",
188
- response_no,
189
- cmd_no,
190
- )
191
- continue
192
- else:
193
- raise IOError(f"Failed to receive reply to command #{cmd_no}.") from err
194
-
195
- def sleepy_send(self, cmd: Union[Command, str]):
196
- self.send("Sleep 0.1")
197
- self.send(cmd)
198
- self.send("Sleep 0.1")
199
-
200
- def send(self, cmd: Union[Command, str]):
201
- """Sends a command to Chemstation.
202
-
203
- :param cmd: Command to be sent to HPLC
204
- """
205
- if self.cmd_no == MAX_CMD_NO:
206
- self.reset_cmd_counter()
207
-
208
- cmd_to_send: str = cmd.value if isinstance(cmd, Command) else cmd
209
- self.cmd_no += 1
210
- self._send(cmd_to_send, self.cmd_no)
211
-
212
- def receive(self) -> str:
213
- """Returns messages received in reply file.
214
-
215
- :return: ChemStation response
216
- """
217
- return self._receive(self.cmd_no)
218
-
219
- def reset_cmd_counter(self):
220
- """Resets the command counter."""
221
- self._send(Command.RESET_COUNTER_CMD.value, cmd_no=MAX_CMD_NO + 1)
222
- self._receive(cmd_no=MAX_CMD_NO + 1)
223
- self.cmd_no = 0
224
-
225
- self.logger.debug("Reset command counter")
226
-
227
- def sleep(self, seconds: int):
228
- """Tells the HPLC to wait for a specified number of seconds.
229
-
230
- :param seconds: number of seconds to wait
231
- """
232
- self.send(Command.SLEEP_CMD.value.format(seconds=seconds))
233
- self.logger.debug("Sleep command sent.")
234
-
235
- def standby(self):
236
- """Switches all modules in standby mode. All lamps and pumps are switched off."""
237
- self.send(Command.STANDBY_CMD)
238
- self.logger.debug("Standby command sent.")
239
-
240
- def preprun(self):
241
- """ Prepares all modules for run. All lamps and pumps are switched on."""
242
- self.send(Command.PREPRUN_CMD)
243
- self.logger.debug("PrepRun command sent.")
244
-
245
- def status(self) -> list[Union[HPLCRunningStatus, HPLCAvailStatus, HPLCErrorStatus]]:
246
- """Get device status(es).
247
-
248
- :return: list of ChemStation's current status
249
- """
250
- self.send(Command.GET_STATUS_CMD)
251
- time.sleep(1)
252
-
253
- try:
254
- parsed_response = self.receive().splitlines()[1].split()[1:]
255
- except IOError:
256
- return [HPLCErrorStatus.NORESPONSE]
257
- except IndexError:
258
- return [HPLCErrorStatus.MALFORMED]
259
- recieved_status = [str_to_status(res) for res in parsed_response]
260
- self._most_recent_hplc_status = recieved_status[0]
261
- return recieved_status
262
-
263
- def stop_macro(self):
264
- """Stops Macro execution. Connection will be lost."""
265
- self.send(Command.STOP_MACRO_CMD)
266
-
267
- def add_table_row(self, table: Table):
268
- """Adds a row to the provided table for currently loaded method or sequence.
269
- Import either the SEQUENCE_TABLE or METHOD_TIMETABLE from hein_analytical_control.constants.
270
- You can also provide your own table.
271
-
272
- :param table: the table to add a new row to
273
- """
274
- self.sleepy_send(TableOperation.NEW_ROW.value.format(register=table.register, table_name=table.name))
275
-
276
- def delete_table(self, table: Table):
277
- """Deletes the table for the current loaded method or sequence.
278
- Import either the SEQUENCE_TABLE or METHOD_TIMETABLE from hein_analytical_control.constants.
279
- You can also provide your own table.
280
-
281
- :param table: the table to delete
282
- """
283
- self.send(TableOperation.DELETE_TABLE.value.format(register=table.register, table_name=table.name))
284
-
285
- def new_table(self, table: Table):
286
- """Creates the table for the currently loaded method or sequence.
287
- Import either the SEQUENCE_TABLE or METHOD_TIMETABLE from hein_analytical_control.constants.
288
- You can also provide your own table.
289
-
290
- :param table: the table to create
291
- """
292
- self.send(TableOperation.CREATE_TABLE.value.format(register=table.register, table_name=table.name))
293
-
294
- def edit_sequence_table(self, sequence_table: SequenceTable):
295
- """Updates the currently loaded sequence table with the provided table. This method will delete the existing sequence table and remake it.
296
- If you would only like to edit a single row of a sequence table, use `edit_sequence_table_row` instead.
297
- :param sequence_table:
298
- """
299
- self.send("Local Rows")
300
- self.sleep(1)
301
- self.delete_table(SEQUENCE_TABLE)
302
- self.sleep(1)
303
- self.new_table(SEQUENCE_TABLE)
304
- self.sleep(1)
305
- self._get_table_rows(SEQUENCE_TABLE)
306
- for i, row in enumerate(sequence_table.rows):
307
- self.add_table_row(SEQUENCE_TABLE)
308
- self.sleep(1)
309
- self.send(Command.SAVE_SEQUENCE_CMD)
310
- self.sleep(1)
311
- self._get_table_rows(SEQUENCE_TABLE)
312
- self.send(Command.SWITCH_SEQUENCE_CMD)
313
- for i, row in enumerate(sequence_table.rows):
314
- self.edit_sequence_table_row(row=row, row_num=i + 1)
315
- self.sleep(1)
316
- self.send(Command.SAVE_SEQUENCE_CMD)
317
- self.sleep(1)
318
-
319
- def edit_sequence_table_row(self, row: SequenceEntry, row_num: int):
320
- """Edits a row in the sequence table. Assumes the row already exists. If not, call `add_sequence_table_row`.
321
- :param row: sequence row entry with updated information
322
- :param row_num: the row to edit, based on 1-based indexing
323
- """
324
- if row.name:
325
- self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(register=SEQUENCE_TABLE.register,
326
- table_name=SEQUENCE_TABLE.name,
327
- row=row_num,
328
- col_name=RegisterFlag.NAME,
329
- val=row.name))
330
- if row.vial_location:
331
- self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(register=SEQUENCE_TABLE.register,
332
- table_name=SEQUENCE_TABLE.name,
333
- row=row_num,
334
- col_name=RegisterFlag.VIAL_LOCATION,
335
- val=row.vial_location))
336
- if row.method:
337
- self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(register=SEQUENCE_TABLE.register,
338
- table_name=SEQUENCE_TABLE.name,
339
- row=row_num,
340
- col_name=RegisterFlag.NAME,
341
- val=row.method))
342
- if row.inj_vol:
343
- self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(register=SEQUENCE_TABLE.register,
344
- table_name=SEQUENCE_TABLE.name,
345
- row=row_num,
346
- col_name=RegisterFlag.NAME,
347
- val=row.inj_vol))
348
-
349
- def edit_method(self, updated_method: MethodTimetable):
350
- """Updated the currently loaded method in ChemStation with provided values.
351
-
352
- :param updated_method: the method with updated values, to be sent to Chemstation to modify the currently loaded method.
353
- """
354
- initial_organic_modifier: Param = updated_method.first_row.organic_modifier
355
- max_time: Param = updated_method.first_row.maximum_run_time
356
- temp: Param = updated_method.first_row.temperature
357
- injvol: Param = updated_method.first_row.inj_vol
358
- equalib_time: Param = updated_method.first_row.equ_time
359
- flow: Param = updated_method.first_row.flow
360
-
361
- # Method settings required for all runs
362
- self.send(TableOperation.DELETE_TABLE.value.format(register=METHOD_TIMETABLE.register,
363
- table_name=METHOD_TIMETABLE.name, ))
364
- self._update_method_param(initial_organic_modifier)
365
- self._update_method_param(flow)
366
- self._update_method_param(Param(val="Set",
367
- chemstation_key=RegisterFlag.STOPTIME_MODE,
368
- ptype=PType.STR))
369
- self._update_method_param(max_time)
370
- self._update_method_param(Param(val="Off",
371
- chemstation_key=RegisterFlag.POSTIME_MODE,
372
- ptype=PType.STR))
373
-
374
- # TODO: set the oven temperature
375
- # self._hplc_controller.update_method_param("str", "Set", "TemperatureControl_Mode")
376
- # self._hplc_controller.update_method_param("num", temp, temp.chemstation_key[0])
377
- # self._hplc_controller.update_method_param("num", temp, temp.chemstation_key[1])
378
- # self._hplc_controller.update_method_param("str", "Set", "TemperatureControl2_Mode")
379
- # self._hplc_controller.send("DownloadRCMethod PMP1")
380
-
381
- self.send("DownloadRCMethod PMP1")
382
-
383
- self._update_method_timetable(updated_method.subsequent_rows)
384
-
385
- def _get_table_rows(self, table: Table) -> str:
386
- self.send(TableOperation.GET_OBJ_HDR_VAL.value.format(internal_val="Row",
387
- register=table.register,
388
- table_name=table.name,
389
- col_name=RegisterFlag.NUM_ROWS, ))
390
- res = self.receive()
391
- self.send("Sleep 1")
392
- self.send('Print Rows')
393
- return res
394
-
395
- def _update_method_timetable(self, timetable_rows: list[Entry]):
396
- """Updates the timetable, which is seen when right-clicking the pump GUI in Chemstation to get to the timetable.
397
- :param updated_method:
398
- """
399
- self.send("Local Rows")
400
-
401
- self.delete_table(METHOD_TIMETABLE)
402
- res = self._get_table_rows(METHOD_TIMETABLE)
403
-
404
- while "ERROR" not in res:
405
- self.delete_table(METHOD_TIMETABLE)
406
- res = self._get_table_rows(METHOD_TIMETABLE)
407
- self.new_table(METHOD_TIMETABLE)
408
- self._get_table_rows(METHOD_TIMETABLE)
409
-
410
- for i, row in enumerate(timetable_rows):
411
- self.send("Sleep 1")
412
- self.send(TableOperation.NEW_ROW.value.format(register=METHOD_TIMETABLE.register,
413
- table_name=METHOD_TIMETABLE.name))
414
- self.send("Sleep 1")
415
-
416
- if i == 0:
417
- self.sleepy_send(TableOperation.NEW_COL_TEXT.value.format(
418
- register=METHOD_TIMETABLE.register,
419
- table_name=METHOD_TIMETABLE.name,
420
- col_name="Function",
421
- val=RegisterFlag.SOLVENT_COMPOSITION.value))
422
- self.sleepy_send(TableOperation.NEW_COL_VAL.value.format(
423
- register=METHOD_TIMETABLE.register,
424
- table_name=METHOD_TIMETABLE.name,
425
- col_name="Time",
426
- val=row.start_time))
427
- self.sleepy_send(TableOperation.NEW_COL_VAL.value.format(
428
- register=METHOD_TIMETABLE.register,
429
- table_name=METHOD_TIMETABLE.name,
430
- col_name=RegisterFlag.SOLVENT_B_COMPOSITION.value,
431
- val=row.organic_modifer))
432
- else:
433
- self.sleepy_send(TableOperation.EDIT_ROW_TEXT.value.format(
434
- register=METHOD_TIMETABLE.register,
435
- table_name=METHOD_TIMETABLE.name,
436
- row="Rows",
437
- col_name="Function",
438
- val=RegisterFlag.SOLVENT_COMPOSITION.value))
439
- self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(
440
- register=METHOD_TIMETABLE.register,
441
- table_name=METHOD_TIMETABLE.name,
442
- row="Rows",
443
- col_name="Time",
444
- val=row.start_time))
445
- self.sleepy_send(TableOperation.EDIT_ROW_VAL.value.format(
446
- register=METHOD_TIMETABLE.register,
447
- table_name=METHOD_TIMETABLE.name,
448
- row="Rows",
449
- col_name=RegisterFlag.SOLVENT_B_COMPOSITION.value,
450
- val=row.organic_modifer))
451
-
452
- self.send("Sleep 1")
453
- self.send("DownloadRCMethod PMP1")
454
- self.send("Sleep 1")
455
-
456
- def _update_method_param(self, method_param: Param):
457
- """Change a method parameter, changes what is visibly seen in Chemstation GUI. (changes the first row in the timetable)
458
-
459
- :param method_param: a parameter to update for currently loaded method.
460
- """
461
- register = METHOD_TIMETABLE.register
462
- setting_command = TableOperation.UPDATE_OBJ_HDR_VAL if method_param.ptype == PType.NUM else TableOperation.UPDATE_OBJ_HDR_TEXT
463
- if isinstance(method_param.chemstation_key, list):
464
- for register_flag in method_param.chemstation_key:
465
- self.send(setting_command.value.format(register=register,
466
- register_flag=register_flag,
467
- val=method_param.val))
468
- else:
469
- self.send(setting_command.value.format(register=register,
470
- register_flag=method_param.chemstation_key,
471
- val=method_param.val))
472
- time.sleep(2)
473
-
474
- def desired_method_already_loaded(self, method_name: str) -> bool:
475
- """Checks if a given method is already loaded into Chemstation. Method name does not need the ".M" extension.
476
-
477
- :param method_name: a Chemstation method
478
- :return: True if method is already loaded
479
- """
480
- self.send(Command.GET_METHOD_CMD)
481
- parsed_response = self.receive().splitlines()[1].split()[1:][0]
482
- return method_name in parsed_response
483
-
484
- def switch_method(self, method_name: str):
485
- """Allows the user to switch between pre-programmed methods. No need to append '.M'
486
- to the end of the method name. For example. for the method named 'General-Poroshell.M',
487
- only 'General-Poroshell' is needed.
488
-
489
- :param method_name: any available method in Chemstation method directory
490
- :raise IndexError: Response did not have expected format. Try again.
491
- :raise AssertionError: The desired method is not selected. Try again.
492
- """
493
- self.send(
494
- Command.SWITCH_METHOD_CMD.value.format(method_dir=self.method_dir, method_name=method_name)
495
- )
496
-
497
- time.sleep(2)
498
- self.send(Command.GET_METHOD_CMD)
499
- time.sleep(2)
500
- # check that method switched
501
- for _ in range(10):
502
- try:
503
- parsed_response = self.receive().splitlines()[1].split()[1:][0]
504
- break
505
- except IndexError:
506
- self.logger.debug("Malformed response. Trying again.")
507
- continue
508
-
509
- assert parsed_response == f"{method_name}.M", "Switching Methods failed."
510
-
511
- def load_method_details(self, method_name: str) -> MethodTimetable:
512
- """Retrieve method details of an existing method. Don't need to append ".M" to the end. This assumes the
513
- organic modifier is in Channel B and that Channel A contains the aq layer. Additionally, assumes
514
- only two solvents are being used.
515
-
516
- :param method_name: name of method to load details of
517
- :raises FileNotFoundError: Method does not exist
518
- :return: method details
519
- """
520
- method_folder = f"{method_name}.M"
521
- method_path = os.path.join(self.method_dir, method_folder, "AgilentPumpDriver1.RapidControl.MethodXML.xml")
522
- dad_path = os.path.join(self.method_dir, method_folder, "Agilent1200erDadDriver1.RapidControl.MethodXML.xml")
523
-
524
- if os.path.exists(os.path.join(self.method_dir, f"{method_name}.M")):
525
- parser = XmlParser()
526
- method = parser.parse(method_path, PumpMethod)
527
- dad = parser.parse(dad_path, DadMethod)
528
-
529
- organic_modifier = None
530
- aq_modifier = None
531
-
532
- if len(method.solvent_composition.solvent_element) == 2:
533
- for solvent in method.solvent_composition.solvent_element:
534
- if solvent.channel == "Channel_A":
535
- aq_modifier = solvent
536
- elif solvent.channel == "Channel_B":
537
- organic_modifier = solvent
538
-
539
- return MethodTimetable(
540
- first_row=HPLCMethodParams(
541
- organic_modifier=Param(val=organic_modifier.percentage,
542
- chemstation_key=RegisterFlag.SOLVENT_B_COMPOSITION,
543
- ptype=PType.NUM),
544
- flow=Param(val=method.flow,
545
- chemstation_key=RegisterFlag.FLOW,
546
- ptype=PType.NUM),
547
- maximum_run_time=Param(val=method.stop_time,
548
- chemstation_key=RegisterFlag.MAX_TIME,
549
- ptype=PType.NUM),
550
- temperature=Param(val=None,
551
- chemstation_key=[RegisterFlag.COLUMN_OVEN_TEMP1,
552
- RegisterFlag.COLUMN_OVEN_TEMP2],
553
- ptype=PType.NUM),
554
- inj_vol=Param(val=None,
555
- chemstation_key=None,
556
- ptype=PType.NUM),
557
- equ_time=Param(val=None,
558
- chemstation_key=None,
559
- ptype=PType.NUM)),
560
- subsequent_rows=[
561
- Entry(
562
- start_time=tte.time,
563
- organic_modifer=tte.percent_b,
564
- flow=method.flow
565
- ) for tte in method.timetable.timetable_entry
566
- ],
567
- dad_wavelengthes=dad.signals.signal,
568
- organic_modifier=organic_modifier,
569
- modifier_a=aq_modifier
570
- )
571
- else:
572
- raise FileNotFoundError
573
-
574
- def lamp_on(self):
575
- """Turns the UV lamp on."""
576
- self.send(Command.LAMP_ON_CMD)
577
-
578
- def lamp_off(self):
579
- """Turns the UV lamp off."""
580
- self.send(Command.LAMP_OFF_CMD)
581
-
582
- def pump_on(self):
583
- """Turns on the pump on."""
584
- self.send(Command.PUMP_ON_CMD)
585
-
586
- def pump_off(self):
587
- """Turns the pump off."""
588
- self.send(Command.PUMP_OFF_CMD)
589
-
590
- def switch_sequence(self, seq_name: str):
591
- """Switch to the specified sequence. The sequence name does not need the '.S' extension.
592
- :param seq_name: The name of the sequence file
593
- :param sequence_dir: The directory where the sequence file resides
594
- """
595
- self.send(f'_SeqFile$ = "{seq_name}.S"')
596
- self.send(f'_SeqPath$ = "{self.sequence_dir}"')
597
- self.send(Command.SWITCH_SEQUENCE_CMD)
598
-
599
- time.sleep(2)
600
- self.send(Command.GET_SEQUENCE_CMD)
601
- time.sleep(2)
602
- # check that method switched
603
- for _ in range(10):
604
- try:
605
- parsed_response = self.receive().splitlines()[1].split()[1:][0]
606
- break
607
- except IndexError:
608
- self.logger.debug("Malformed response. Trying again.")
609
- continue
610
-
611
- assert parsed_response == f"{seq_name}.S", "Switching sequence failed."
612
-
613
- def run_sequence(self, experiment_name: str):
614
- """Starts the currently loaded sequence, storing data
615
- under the <data_dir>/<experiment_name>.D folder.
616
- The <experiment_name> will be appended with a timestamp in the '%Y-%m-%d-%H-%M' format.
617
- Device must be ready.
618
-
619
- :param experiment_name: Name of the experiment
620
- """
621
- timestamp = time.strftime(TIME_FORMAT)
622
-
623
- self.send(
624
- Command.RUN_SEQUENCE_CMD.value.format(
625
- data_dir=self.data_dir, experiment_name=experiment_name, timestamp=timestamp
626
- )
627
- )
628
-
629
- folder_name = f"{experiment_name}_{timestamp}.D"
630
- self.data_files.append(os.path.join(self.data_dir, folder_name))
631
- self.logger.info("Started HPLC run: %s.", folder_name)
632
-
633
- def start_method(self):
634
- """Starts and executes currently loaded method to run according to Run Time Checklist. Device must be ready.
635
- Does not store the folder where data is saved.
636
- """
637
- self.send(Command.START_METHOD_CMD)
638
-
639
- def run_method(self, experiment_name: str):
640
- """This is the preferred method to trigger a run.
641
- Starts the currently selected method, storing data
642
- under the <data_dir>/<experiment_name>.D folder.
643
- The <experiment_name> will be appended with a timestamp in the '%Y-%m-%d-%H-%M' format.
644
- Device must be ready.
645
-
646
- :param experiment_name: Name of the experiment
647
- """
648
- timestamp = time.strftime(TIME_FORMAT)
649
-
650
- self.send(
651
- Command.RUN_METHOD_CMD.value.format(
652
- data_dir=self.data_dir, experiment_name=experiment_name, timestamp=timestamp
653
- )
654
- )
655
-
656
- folder_name = f"{experiment_name}_{timestamp}.D"
657
- self.data_files.append(os.path.join(self.data_dir, folder_name))
658
- self.logger.info("Started HPLC run: %s.", folder_name)
659
-
660
- def stop_method(self):
661
- """Stops the run. A dialog window will pop up and manual intervention may be required."""
662
- self.send(Command.STOP_METHOD_CMD)
663
-
664
- def get_spectrum(self):
665
- """ Load last chromatogram for any channel in spectra dictionary."""
666
- last_file = self.data_files[-1] if len(self.data_files) > 0 else None
667
-
668
- if last_file is None:
669
- raise IndexError
670
-
671
- for channel, spec in self.spectra.items():
672
- spec.load_spectrum(data_path=last_file, channel=channel)
673
- self.logger.info("%s chromatogram loaded.", channel)