pychemstation 0.4.7.dev1__tar.gz → 0.4.7.dev3__tar.gz

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 (37) hide show
  1. {pychemstation-0.4.7.dev1 → pychemstation-0.4.7.dev3}/PKG-INFO +13 -12
  2. {pychemstation-0.4.7.dev1 → pychemstation-0.4.7.dev3}/README.md +12 -11
  3. pychemstation-0.4.7.dev3/pychemstation/control/__init__.py +6 -0
  4. pychemstation-0.4.7.dev3/pychemstation/control/comm.py +276 -0
  5. pychemstation-0.4.7.dev3/pychemstation/control/method.py +232 -0
  6. pychemstation-0.4.7.dev3/pychemstation/control/sequence.py +140 -0
  7. pychemstation-0.4.7.dev3/pychemstation/control/table_controller.py +75 -0
  8. pychemstation-0.4.7.dev3/pychemstation/utils/__init__.py +0 -0
  9. {pychemstation-0.4.7.dev1/pychemstation/control → pychemstation-0.4.7.dev3/pychemstation/utils}/chromatogram.py +2 -1
  10. {pychemstation-0.4.7.dev1 → pychemstation-0.4.7.dev3}/pychemstation/utils/constants.py +1 -1
  11. pychemstation-0.4.7.dev3/pychemstation/utils/macro.py +77 -0
  12. pychemstation-0.4.7.dev3/pychemstation/utils/method_types.py +44 -0
  13. pychemstation-0.4.7.dev3/pychemstation/utils/sequence_types.py +33 -0
  14. pychemstation-0.4.7.dev3/pychemstation/utils/table_types.py +60 -0
  15. {pychemstation-0.4.7.dev1 → pychemstation-0.4.7.dev3}/pychemstation.egg-info/PKG-INFO +13 -12
  16. {pychemstation-0.4.7.dev1 → pychemstation-0.4.7.dev3}/pychemstation.egg-info/SOURCES.txt +10 -4
  17. {pychemstation-0.4.7.dev1 → pychemstation-0.4.7.dev3}/pyproject.toml +2 -2
  18. {pychemstation-0.4.7.dev1 → pychemstation-0.4.7.dev3}/setup.py +1 -1
  19. {pychemstation-0.4.7.dev1 → pychemstation-0.4.7.dev3}/tests/test_chemstation_integration.py +52 -62
  20. pychemstation-0.4.7.dev1/pychemstation/control/__init__.py +0 -5
  21. pychemstation-0.4.7.dev1/pychemstation/control/hplc.py +0 -673
  22. pychemstation-0.4.7.dev1/pychemstation/utils/__init__.py +0 -2
  23. pychemstation-0.4.7.dev1/pychemstation/utils/hplc_param_types.py +0 -185
  24. {pychemstation-0.4.7.dev1 → pychemstation-0.4.7.dev3}/LICENSE +0 -0
  25. {pychemstation-0.4.7.dev1 → pychemstation-0.4.7.dev3}/pychemstation/__init__.py +0 -0
  26. {pychemstation-0.4.7.dev1 → pychemstation-0.4.7.dev3}/pychemstation/analysis/__init__.py +0 -0
  27. {pychemstation-0.4.7.dev1 → pychemstation-0.4.7.dev3}/pychemstation/analysis/base_spectrum.py +0 -0
  28. {pychemstation-0.4.7.dev1 → pychemstation-0.4.7.dev3}/pychemstation/analysis/spec_utils.py +0 -0
  29. {pychemstation-0.4.7.dev1 → pychemstation-0.4.7.dev3}/pychemstation/analysis/utils.py +0 -0
  30. {pychemstation-0.4.7.dev1 → pychemstation-0.4.7.dev3}/pychemstation/generated/__init__.py +0 -0
  31. {pychemstation-0.4.7.dev1 → pychemstation-0.4.7.dev3}/pychemstation/generated/dad_method.py +0 -0
  32. {pychemstation-0.4.7.dev1 → pychemstation-0.4.7.dev3}/pychemstation/generated/pump_method.py +0 -0
  33. /pychemstation-0.4.7.dev1/pychemstation/utils/chemstation.py → /pychemstation-0.4.7.dev3/pychemstation/utils/parsing.py +0 -0
  34. {pychemstation-0.4.7.dev1 → pychemstation-0.4.7.dev3}/pychemstation.egg-info/dependency_links.txt +0 -0
  35. {pychemstation-0.4.7.dev1 → pychemstation-0.4.7.dev3}/pychemstation.egg-info/requires.txt +0 -0
  36. {pychemstation-0.4.7.dev1 → pychemstation-0.4.7.dev3}/pychemstation.egg-info/top_level.txt +0 -0
  37. {pychemstation-0.4.7.dev1 → pychemstation-0.4.7.dev3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pychemstation
3
- Version: 0.4.7.dev1
3
+ Version: 0.4.7.dev3
4
4
  Summary: Library to interact with Chemstation software, primarily used in Hein lab
5
5
  Home-page: https://gitlab.com/heingroup/pychemstation
6
6
  Author: Lucy Hao
@@ -15,8 +15,9 @@ Requires-Dist: seabreeze
15
15
 
16
16
  # Agilent HPLC Macro Control
17
17
 
18
- [![PyPI Downloads](https://img.shields.io/pypi/dm/hein-analytical-control.svg?label=PyPI%20downloads)](https://pypi.org/project/hein-analytical-control)
19
- [![PyPI Latest Release](https://img.shields.io/pypi/v/hein-analytical-control.svg)](https://pypi.org/project/hein-analytical-control/)
18
+ ![PyPI - Downloads](https://img.shields.io/pypi/dm/pychemstation)
19
+
20
+ [![PyPI Latest Release](https://img.shields.io/pypi/v/pychemstation.svg)](https://pypi.org/project/pychemstation/)
20
21
 
21
22
  Unofficial Python package to control Agilent Chemstation; we are not affiliated with Agilent.
22
23
  Check out the [docs](https://hein-analytical-control-5e6e85.gitlab.io/) for usage instructions. This project is under
@@ -58,7 +59,7 @@ HPLCTalk_Run
58
59
  ## Example Usage
59
60
 
60
61
  ```python
61
- from pychemstation.control import HPLCController
62
+ from pychemstation.control import HPLCController, MethodController, SequenceController
62
63
  import pandas as pd
63
64
 
64
65
  # these paths will be unique to your Chemstation setup
@@ -67,17 +68,17 @@ DATA_DIR = "C:\\Users\\Public\\Documents\\ChemStation\\2\\Data"
67
68
  DEFAULT_COMMAND_PATH = "C:\\Users\\User\\Desktop\\Lucy\\hplc-method-optimization\\tests"
68
69
 
69
70
  hplc_controller = HPLCController(data_dir=DATA_DIR,
70
- comm_dir=DEFAULT_COMMAND_PATH,
71
- method_dir=DEFAULT_METHOD_DIR)
71
+ comm_dir=DEFAULT_COMMAND_PATH)
72
+ method_controller = MethodController(controller=hplc_controller,
73
+ src=DEFAULT_METHOD_DIR)
72
74
 
73
75
  hplc_controller.preprun()
74
- hplc_controller.switch_method(method_name="General-Poroshell")
75
- hplc_controller.run_method(experiment_name="Run 10")
76
- data_ready = hplc_controller.check_hplc_ready_with_data()
76
+ method_controller.switch(method_name="General-Poroshell")
77
+ method_controller.run(experiment_name="Run 10")
78
+ data_ready = method_controller.data_ready()
77
79
 
78
80
  if data_ready:
79
- hplc_controller.get_spectrum()
80
- chrom = hplc_controller.spectra["A"]
81
+ chrom = method_controller.get_data()
81
82
  # afterwards, save, analyze or plot the data!
82
83
  values = {"x": chrom.x, "y": chrom.y}
83
84
  chromatogram_data = pd.DataFrame.from_dict(values)
@@ -92,7 +93,7 @@ put the file in the `user.mac` file and then list the function you want to use.
92
93
  ## Developing
93
94
 
94
95
  If you would like to contribute to this project, check out
95
- our [GitLab](https://gitlab.com/heingroup/hein-analytical-control)!
96
+ our [GitLab](https://gitlab.com/heingroup/device-api/pychemstation)!
96
97
 
97
98
  ## Authors and Acknowledgements
98
99
 
@@ -1,7 +1,8 @@
1
1
  # Agilent HPLC Macro Control
2
2
 
3
- [![PyPI Downloads](https://img.shields.io/pypi/dm/hein-analytical-control.svg?label=PyPI%20downloads)](https://pypi.org/project/hein-analytical-control)
4
- [![PyPI Latest Release](https://img.shields.io/pypi/v/hein-analytical-control.svg)](https://pypi.org/project/hein-analytical-control/)
3
+ ![PyPI - Downloads](https://img.shields.io/pypi/dm/pychemstation)
4
+
5
+ [![PyPI Latest Release](https://img.shields.io/pypi/v/pychemstation.svg)](https://pypi.org/project/pychemstation/)
5
6
 
6
7
  Unofficial Python package to control Agilent Chemstation; we are not affiliated with Agilent.
7
8
  Check out the [docs](https://hein-analytical-control-5e6e85.gitlab.io/) for usage instructions. This project is under
@@ -43,7 +44,7 @@ HPLCTalk_Run
43
44
  ## Example Usage
44
45
 
45
46
  ```python
46
- from pychemstation.control import HPLCController
47
+ from pychemstation.control import HPLCController, MethodController, SequenceController
47
48
  import pandas as pd
48
49
 
49
50
  # these paths will be unique to your Chemstation setup
@@ -52,17 +53,17 @@ DATA_DIR = "C:\\Users\\Public\\Documents\\ChemStation\\2\\Data"
52
53
  DEFAULT_COMMAND_PATH = "C:\\Users\\User\\Desktop\\Lucy\\hplc-method-optimization\\tests"
53
54
 
54
55
  hplc_controller = HPLCController(data_dir=DATA_DIR,
55
- comm_dir=DEFAULT_COMMAND_PATH,
56
- method_dir=DEFAULT_METHOD_DIR)
56
+ comm_dir=DEFAULT_COMMAND_PATH)
57
+ method_controller = MethodController(controller=hplc_controller,
58
+ src=DEFAULT_METHOD_DIR)
57
59
 
58
60
  hplc_controller.preprun()
59
- hplc_controller.switch_method(method_name="General-Poroshell")
60
- hplc_controller.run_method(experiment_name="Run 10")
61
- data_ready = hplc_controller.check_hplc_ready_with_data()
61
+ method_controller.switch(method_name="General-Poroshell")
62
+ method_controller.run(experiment_name="Run 10")
63
+ data_ready = method_controller.data_ready()
62
64
 
63
65
  if data_ready:
64
- hplc_controller.get_spectrum()
65
- chrom = hplc_controller.spectra["A"]
66
+ chrom = method_controller.get_data()
66
67
  # afterwards, save, analyze or plot the data!
67
68
  values = {"x": chrom.x, "y": chrom.y}
68
69
  chromatogram_data = pd.DataFrame.from_dict(values)
@@ -77,7 +78,7 @@ put the file in the `user.mac` file and then list the function you want to use.
77
78
  ## Developing
78
79
 
79
80
  If you would like to contribute to this project, check out
80
- our [GitLab](https://gitlab.com/heingroup/hein-analytical-control)!
81
+ our [GitLab](https://gitlab.com/heingroup/device-api/pychemstation)!
81
82
 
82
83
  ## Authors and Acknowledgements
83
84
 
@@ -0,0 +1,6 @@
1
+ """
2
+ .. include:: README.md
3
+ """
4
+ from .comm import HPLCController
5
+ from .method import MethodController
6
+ from .sequence import SequenceController
@@ -0,0 +1,276 @@
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
+
17
+ import polling
18
+
19
+ from ..utils.chromatogram import AgilentHPLCChromatogram
20
+ from ..utils.constants import MAX_CMD_NO
21
+ from ..utils.macro import *
22
+ from ..utils.method_types import *
23
+
24
+
25
+ class HPLCController:
26
+ """
27
+ Class to control Agilent HPLC systems via Chemstation Macros.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ comm_dir: str,
33
+ data_dir: str,
34
+ cmd_file: str = "cmd",
35
+ reply_file: str = "reply",
36
+ ):
37
+ """Initialize HPLC controller. The `hplc_talk.mac` macro file must be loaded in the Chemstation software.
38
+ `comm_dir` must match the file path in the macro file.
39
+
40
+ :param comm_dir: Name of directory for communication, where ChemStation will read and write from. Can be any existing directory.
41
+ :param data_dir: Name of directory that ChemStation saves run data. Must be accessible by ChemStation.
42
+ :param cmd_file: Name of command file
43
+ :param reply_file: Name of reply file
44
+ :raises FileNotFoundError: If either `data_dir`, `method_dir` or `comm_dir` is not a valid directory.
45
+ """
46
+ if os.path.isdir(comm_dir):
47
+ self.cmd_file = os.path.join(comm_dir, cmd_file)
48
+ self.reply_file = os.path.join(comm_dir, reply_file)
49
+ self.cmd_no = 0
50
+ else:
51
+ raise FileNotFoundError(f"comm_dir: {comm_dir} not found.")
52
+ self._most_recent_hplc_status = None
53
+
54
+ if os.path.isdir(data_dir):
55
+ self.data_dir = data_dir
56
+ else:
57
+ raise FileNotFoundError(f"data_dir: {data_dir} not found.")
58
+
59
+ self.spectra = {
60
+ "A": AgilentHPLCChromatogram(self.data_dir),
61
+ "B": AgilentHPLCChromatogram(self.data_dir),
62
+ "C": AgilentHPLCChromatogram(self.data_dir),
63
+ "D": AgilentHPLCChromatogram(self.data_dir),
64
+ }
65
+
66
+ self.data_files: list[str] = []
67
+ self.internal_variables: list[dict[str, str]] = []
68
+
69
+ # Create files for Chemstation to communicate with Python
70
+ open(self.cmd_file, "a").close()
71
+ open(self.reply_file, "a").close()
72
+
73
+ self.logger = logging.getLogger("hplc_logger")
74
+ self.logger.addHandler(logging.NullHandler())
75
+
76
+ self.reset_cmd_counter()
77
+
78
+ self.logger.info("HPLC Controller initialized.")
79
+
80
+ def _set_status(self):
81
+ """Updates current status of HPLC machine"""
82
+ self._most_recent_hplc_status = self.status()[0]
83
+
84
+ def _check_data_status(self) -> bool:
85
+ """Checks if HPLC machine is in an available state, meaning a state that data is not being written.
86
+
87
+ :return: whether the HPLC machine is in a safe state to retrieve data back."""
88
+ old_status = self._most_recent_hplc_status
89
+ self._set_status()
90
+ file_exists = os.path.exists(self.data_files[-1]) if len(self.data_files) > 0 else False
91
+ done_writing_data = isinstance(self._most_recent_hplc_status,
92
+ HPLCAvailStatus) and old_status != self._most_recent_hplc_status and file_exists
93
+ return done_writing_data
94
+
95
+ def check_hplc_ready_with_data(self) -> bool:
96
+ """Checks if ChemStation has finished writing data and can be read back.
97
+
98
+ :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
99
+
100
+ :return: Return True if data can be read back, else False.
101
+ """
102
+ self._set_status()
103
+
104
+ timeout = 10 * 60
105
+ hplc_run_done = polling.poll(
106
+ lambda: self._check_data_status(),
107
+ timeout=timeout,
108
+ step=30
109
+ )
110
+
111
+ return hplc_run_done
112
+
113
+ def get_spectrum(self):
114
+ """ Load last chromatogram for any channel in spectra dictionary."""
115
+ last_file = self.data_files[-1] if len(self.data_files) > 0 else None
116
+
117
+ if last_file is None:
118
+ raise IndexError
119
+
120
+ for channel, spec in self.controller.spectra.items():
121
+ spec.load_spectrum(data_path=last_file, channel=channel)
122
+ self.logger.info("%s chromatogram loaded.", channel)
123
+
124
+ def _send(self, cmd: str, cmd_no: int, num_attempts=5) -> None:
125
+ """Low-level execution primitive. Sends a command string to HPLC.
126
+
127
+ :param cmd: string to be sent to HPLC
128
+ :param cmd_no: Command number
129
+ :param num_attempts: Number of attempts to send the command before raising exception.
130
+ :raises IOError: Could not write to command file.
131
+ """
132
+ err = None
133
+ for _ in range(num_attempts):
134
+ time.sleep(1)
135
+ try:
136
+ with open(self.cmd_file, "w", encoding="utf8") as cmd_file:
137
+ cmd_file.write(f"{cmd_no} {cmd}")
138
+ except IOError as e:
139
+ err = e
140
+ self.logger.warning("Failed to send command; trying again.")
141
+ continue
142
+ else:
143
+ self.logger.info("Sent command #%d: %s.", cmd_no, cmd)
144
+ return
145
+ else:
146
+ raise IOError(f"Failed to send command #{cmd_no}: {cmd}.") from err
147
+
148
+ def _receive(self, cmd_no: int, num_attempts=100) -> str:
149
+ """Low-level execution primitive. Recives a response from HPLC.
150
+
151
+ :param cmd_no: Command number
152
+ :param num_attempts: Number of retries to open reply file
153
+ :raises IOError: Could not read reply file.
154
+ :return: ChemStation response
155
+ """
156
+ err = None
157
+ for _ in range(num_attempts):
158
+ time.sleep(1)
159
+
160
+ try:
161
+ with open(self.reply_file, "r", encoding="utf_16") as reply_file:
162
+ response = reply_file.read()
163
+ except OSError as e:
164
+ err = e
165
+ self.logger.warning("Failed to read from reply file; trying again.")
166
+ continue
167
+
168
+ try:
169
+ first_line = response.splitlines()[0]
170
+ response_no = int(first_line.split()[0])
171
+ except IndexError as e:
172
+ err = e
173
+ self.logger.warning("Malformed response %s; trying again.", response)
174
+ continue
175
+
176
+ # check that response corresponds to sent command
177
+ if response_no == cmd_no:
178
+ self.logger.info("Reply: \n%s", response)
179
+ return response
180
+ else:
181
+ self.logger.warning(
182
+ "Response #: %d != command #: %d; trying again.",
183
+ response_no,
184
+ cmd_no,
185
+ )
186
+ continue
187
+ else:
188
+ raise IOError(f"Failed to receive reply to command #{cmd_no}.") from err
189
+
190
+ def sleepy_send(self, cmd: Union[Command, str]):
191
+ self.send("Sleep 0.1")
192
+ self.send(cmd)
193
+ self.send("Sleep 0.1")
194
+
195
+ def send(self, cmd: Union[Command, str]):
196
+ """Sends a command to Chemstation.
197
+
198
+ :param cmd: Command to be sent to HPLC
199
+ """
200
+ if self.cmd_no == MAX_CMD_NO:
201
+ self.reset_cmd_counter()
202
+
203
+ cmd_to_send: str = cmd.value if isinstance(cmd, Command) else cmd
204
+ self.cmd_no += 1
205
+ self._send(cmd_to_send, self.cmd_no)
206
+
207
+ def receive(self) -> str:
208
+ """Returns messages received in reply file.
209
+
210
+ :return: ChemStation response
211
+ """
212
+ return self._receive(self.cmd_no)
213
+
214
+ def reset_cmd_counter(self):
215
+ """Resets the command counter."""
216
+ self._send(Command.RESET_COUNTER_CMD.value, cmd_no=MAX_CMD_NO + 1)
217
+ self._receive(cmd_no=MAX_CMD_NO + 1)
218
+ self.cmd_no = 0
219
+
220
+ self.logger.debug("Reset command counter")
221
+
222
+ def sleep(self, seconds: int):
223
+ """Tells the HPLC to wait for a specified number of seconds.
224
+
225
+ :param seconds: number of seconds to wait
226
+ """
227
+ self.send(Command.SLEEP_CMD.value.format(seconds=seconds))
228
+ self.logger.debug("Sleep command sent.")
229
+
230
+ def standby(self):
231
+ """Switches all modules in standby mode. All lamps and pumps are switched off."""
232
+ self.send(Command.STANDBY_CMD)
233
+ self.logger.debug("Standby command sent.")
234
+
235
+ def preprun(self):
236
+ """ Prepares all modules for run. All lamps and pumps are switched on."""
237
+ self.send(Command.PREPRUN_CMD)
238
+ self.logger.debug("PrepRun command sent.")
239
+
240
+ def status(self) -> list[Union[HPLCRunningStatus, HPLCAvailStatus, HPLCErrorStatus]]:
241
+ """Get device status(es).
242
+
243
+ :return: list of ChemStation's current status
244
+ """
245
+ self.send(Command.GET_STATUS_CMD)
246
+ time.sleep(1)
247
+
248
+ try:
249
+ parsed_response = self.receive().splitlines()[1].split()[1:]
250
+ except IOError:
251
+ return [HPLCErrorStatus.NORESPONSE]
252
+ except IndexError:
253
+ return [HPLCErrorStatus.MALFORMED]
254
+ recieved_status = [str_to_status(res) for res in parsed_response]
255
+ self._most_recent_hplc_status = recieved_status[0]
256
+ return recieved_status
257
+
258
+ def stop_macro(self):
259
+ """Stops Macro execution. Connection will be lost."""
260
+ self.send(Command.STOP_MACRO_CMD)
261
+
262
+ def lamp_on(self):
263
+ """Turns the UV lamp on."""
264
+ self.send(Command.LAMP_ON_CMD)
265
+
266
+ def lamp_off(self):
267
+ """Turns the UV lamp off."""
268
+ self.send(Command.LAMP_OFF_CMD)
269
+
270
+ def pump_on(self):
271
+ """Turns on the pump on."""
272
+ self.send(Command.PUMP_ON_CMD)
273
+
274
+ def pump_off(self):
275
+ """Turns the pump off."""
276
+ self.send(Command.PUMP_OFF_CMD)
@@ -0,0 +1,232 @@
1
+ import os
2
+ import time
3
+
4
+ from xsdata.formats.dataclass.parsers import XmlParser
5
+
6
+ from ..control.table_controller import TableController, HPLCController
7
+ from ..generated import DadMethod, PumpMethod
8
+ from ..utils.chromatogram import TIME_FORMAT
9
+ from ..utils.constants import METHOD_TIMETABLE
10
+ from ..utils.macro import Command
11
+ from ..utils.method_types import MethodTimetable, HPLCMethodParams, Param, PType, TimeTableEntry
12
+ from ..utils.table_types import RegisterFlag, TableOperation
13
+
14
+
15
+ class MethodController(TableController):
16
+ """
17
+ Class containing method related logic
18
+ """
19
+
20
+ def __init__(self, controller: HPLCController, src: str):
21
+ super().__init__(controller, src)
22
+
23
+ def is_loaded(self, method_name: str):
24
+ """Checks if a given method is already loaded into Chemstation. Method name does not need the ".M" extension.
25
+
26
+ :param method_name: a Chemstation method
27
+ :return: True if method is already loaded
28
+ """
29
+ self.send(Command.GET_METHOD_CMD)
30
+ parsed_response = self.receive().splitlines()[1].split()[1:][0]
31
+ return method_name in parsed_response
32
+
33
+ def switch(self, method_name: str):
34
+ """Allows the user to switch between pre-programmed methods. No need to append '.M'
35
+ to the end of the method name. For example. for the method named 'General-Poroshell.M',
36
+ only 'General-Poroshell' is needed.
37
+
38
+ :param method_name: any available method in Chemstation method directory
39
+ :raise IndexError: Response did not have expected format. Try again.
40
+ :raise AssertionError: The desired method is not selected. Try again.
41
+ """
42
+ self.send(Command.SWITCH_METHOD_CMD.value.format(method_dir=self.src,
43
+ method_name=method_name))
44
+
45
+ time.sleep(2)
46
+ self.send(Command.GET_METHOD_CMD)
47
+ time.sleep(2)
48
+
49
+ # check that method switched
50
+ for _ in range(10):
51
+ try:
52
+ parsed_response = self.receive().splitlines()[1].split()[1:][0]
53
+ break
54
+ except IndexError:
55
+ continue
56
+
57
+ assert parsed_response == f"{method_name}.M", "Switching Methods failed."
58
+
59
+ def run(self, experiment_name: str):
60
+ """This is the preferred method to trigger a run.
61
+ Starts the currently selected method, storing data
62
+ under the <data_dir>/<experiment_name>.D folder.
63
+ The <experiment_name> will be appended with a timestamp in the '%Y-%m-%d-%H-%M' format.
64
+ Device must be ready.
65
+
66
+ :param experiment_name: Name of the experiment
67
+ """
68
+ timestamp = time.strftime(TIME_FORMAT)
69
+
70
+ self.send(Command.RUN_METHOD_CMD.value.format(data_dir=self.src,
71
+ experiment_name=experiment_name,
72
+ timestamp=timestamp))
73
+
74
+ folder_name = f"{experiment_name}_{timestamp}.D"
75
+ self.controller.data_files.append(os.path.join(self.src, folder_name))
76
+
77
+ def load(self, method_name: str):
78
+ """Retrieve method details of an existing method. Don't need to append ".M" to the end. This assumes the
79
+ organic modifier is in Channel B and that Channel A contains the aq layer. Additionally, assumes
80
+ only two solvents are being used.
81
+
82
+ :param method_name: name of method to load details of
83
+ :raises FileNotFoundError: Method does not exist
84
+ :return: method details
85
+ """
86
+ method_folder = f"{method_name}.M"
87
+ method_path = os.path.join(self.src, method_folder, "AgilentPumpDriver1.RapidControl.MethodXML.xml")
88
+ dad_path = os.path.join(self.src, method_folder, "Agilent1200erDadDriver1.RapidControl.MethodXML.xml")
89
+
90
+ if os.path.exists(os.path.join(self.src, f"{method_name}.M")):
91
+ parser = XmlParser()
92
+ method = parser.parse(method_path, PumpMethod)
93
+ dad = parser.parse(dad_path, DadMethod)
94
+
95
+ organic_modifier = None
96
+ aq_modifier = None
97
+
98
+ if len(method.solvent_composition.solvent_element) == 2:
99
+ for solvent in method.solvent_composition.solvent_element:
100
+ if solvent.channel == "Channel_A":
101
+ aq_modifier = solvent
102
+ elif solvent.channel == "Channel_B":
103
+ organic_modifier = solvent
104
+
105
+ return MethodTimetable(
106
+ first_row=HPLCMethodParams(
107
+ organic_modifier=Param(val=organic_modifier.percentage,
108
+ chemstation_key=RegisterFlag.SOLVENT_B_COMPOSITION,
109
+ ptype=PType.NUM),
110
+ flow=Param(val=method.flow,
111
+ chemstation_key=RegisterFlag.FLOW,
112
+ ptype=PType.NUM),
113
+ maximum_run_time=Param(val=method.stop_time,
114
+ chemstation_key=RegisterFlag.MAX_TIME,
115
+ ptype=PType.NUM),
116
+ temperature=Param(val=None,
117
+ chemstation_key=[RegisterFlag.COLUMN_OVEN_TEMP1,
118
+ RegisterFlag.COLUMN_OVEN_TEMP2],
119
+ ptype=PType.NUM),
120
+ inj_vol=Param(val=None,
121
+ chemstation_key=None,
122
+ ptype=PType.NUM),
123
+ equ_time=Param(val=None,
124
+ chemstation_key=None,
125
+ ptype=PType.NUM)),
126
+ subsequent_rows=[
127
+ TimeTableEntry(
128
+ start_time=tte.time,
129
+ organic_modifer=tte.percent_b,
130
+ flow=method.flow
131
+ ) for tte in method.timetable.timetable_entry
132
+ ],
133
+ dad_wavelengthes=dad.signals.signal,
134
+ organic_modifier=organic_modifier,
135
+ modifier_a=aq_modifier
136
+ )
137
+ else:
138
+ raise FileNotFoundError
139
+
140
+ def edit(self, updated_method: MethodTimetable):
141
+ """Updated the currently loaded method in ChemStation with provided values.
142
+
143
+ :param updated_method: the method with updated values, to be sent to Chemstation to modify the currently loaded method.
144
+ """
145
+ initial_organic_modifier: Param = updated_method.first_row.organic_modifier
146
+ max_time: Param = updated_method.first_row.maximum_run_time
147
+ temp: Param = updated_method.first_row.temperature
148
+ injvol: Param = updated_method.first_row.inj_vol
149
+ equalib_time: Param = updated_method.first_row.equ_time
150
+ flow: Param = updated_method.first_row.flow
151
+
152
+ # Method settings required for all runs
153
+ self.send(TableOperation.DELETE_TABLE.value.format(register=METHOD_TIMETABLE.register,
154
+ table_name=METHOD_TIMETABLE.name, ))
155
+ self._update_param(initial_organic_modifier)
156
+ self._update_param(flow)
157
+ self._update_param(Param(val="Set",
158
+ chemstation_key=RegisterFlag.STOPTIME_MODE,
159
+ ptype=PType.STR))
160
+ self._update_param(max_time)
161
+ self._update_param(Param(val="Off",
162
+ chemstation_key=RegisterFlag.POSTIME_MODE,
163
+ ptype=PType.STR))
164
+
165
+ self.send("DownloadRCMethod PMP1")
166
+
167
+ self._update_method_timetable(updated_method.subsequent_rows)
168
+
169
+ def _update_method_timetable(self, timetable_rows: list[TimeTableEntry]):
170
+ self.sleepy_send('Local Rows')
171
+ self._get_table_rows(METHOD_TIMETABLE)
172
+
173
+ self.sleepy_send('DelTab RCPMP1Method[1], "Timetable"')
174
+ res = self._get_table_rows(METHOD_TIMETABLE)
175
+ while "ERROR" not in res:
176
+ self.sleepy_send('DelTab RCPMP1Method[1], "Timetable"')
177
+ res = self._get_table_rows(METHOD_TIMETABLE)
178
+
179
+ self.sleepy_send('NewTab RCPMP1Method[1], "Timetable"')
180
+ self._get_table_rows(METHOD_TIMETABLE)
181
+
182
+ for i, row in enumerate(timetable_rows):
183
+ if i == 0:
184
+ self.send('Sleep 1')
185
+ self.sleepy_send('InsTabRow RCPMP1Method[1], "Timetable"')
186
+ self.send('Sleep 1')
187
+
188
+ self.sleepy_send('NewColText RCPMP1Method[1], "Timetable", "Function", "SolventComposition"')
189
+ self.sleepy_send(f'NewColVal RCPMP1Method[1], "Timetable", "Time", {row.start_time}')
190
+ self.sleepy_send(
191
+ f'NewColVal RCPMP1Method[1], "Timetable", "SolventCompositionPumpChannel2_Percentage", {row.organic_modifer}')
192
+
193
+ self.send('Sleep 1')
194
+ self.sleepy_send("DownloadRCMethod PMP1")
195
+ self.send('Sleep 1')
196
+ else:
197
+ self.sleepy_send('InsTabRow RCPMP1Method[1], "Timetable"')
198
+ self._get_table_rows(METHOD_TIMETABLE)
199
+
200
+ self.sleepy_send(
201
+ f'SetTabText RCPMP1Method[1], "Timetable", Rows, "Function", "SolventComposition"')
202
+ self.sleepy_send(
203
+ f'SetTabVal RCPMP1Method[1], "Timetable", Rows, "Time", {row.start_time}')
204
+ self.sleepy_send(
205
+ f'SetTabVal RCPMP1Method[1], "Timetable", Rows, "SolventCompositionPumpChannel2_Percentage", {row.organic_modifer}')
206
+
207
+ self.send("Sleep 1")
208
+ self.sleepy_send("DownloadRCMethod PMP1")
209
+ self.send("Sleep 1")
210
+ self._get_table_rows(METHOD_TIMETABLE)
211
+
212
+ def _update_param(self, method_param: Param):
213
+ """Change a method parameter, changes what is visibly seen in Chemstation GUI. (changes the first row in the timetable)
214
+
215
+ :param method_param: a parameter to update for currently loaded method.
216
+ """
217
+ register = METHOD_TIMETABLE.register
218
+ setting_command = TableOperation.UPDATE_OBJ_HDR_VAL if method_param.ptype == PType.NUM else TableOperation.UPDATE_OBJ_HDR_TEXT
219
+ if isinstance(method_param.chemstation_key, list):
220
+ for register_flag in method_param.chemstation_key:
221
+ self.send(setting_command.value.format(register=register,
222
+ register_flag=register_flag,
223
+ val=method_param.val))
224
+ else:
225
+ self.send(setting_command.value.format(register=register,
226
+ register_flag=method_param.chemstation_key,
227
+ val=method_param.val))
228
+ time.sleep(2)
229
+
230
+ def stop(self):
231
+ """Stops the run. A dialog window will pop up and manual intervention may be required."""
232
+ self.send(Command.STOP_METHOD_CMD)