pychemstation 0.4.7.dev1__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.
- ag_hplc_macro/__init__.py +3 -0
- ag_hplc_macro/analysis/__init__.py +1 -0
- ag_hplc_macro/analysis/base_spectrum.py +509 -0
- ag_hplc_macro/analysis/spec_utils.py +304 -0
- ag_hplc_macro/analysis/utils.py +63 -0
- ag_hplc_macro/control/__init__.py +5 -0
- ag_hplc_macro/control/chromatogram.py +128 -0
- ag_hplc_macro/control/hplc.py +673 -0
- ag_hplc_macro/generated/__init__.py +56 -0
- ag_hplc_macro/generated/dad_method.py +367 -0
- ag_hplc_macro/generated/pump_method.py +519 -0
- ag_hplc_macro/utils/__init__.py +2 -0
- ag_hplc_macro/utils/chemstation.py +290 -0
- ag_hplc_macro/utils/constants.py +15 -0
- ag_hplc_macro/utils/hplc_param_types.py +185 -0
- hein-analytical-control/__init__.py +3 -0
- hein-analytical-control/analysis/__init__.py +1 -0
- hein-analytical-control/analysis/base_spectrum.py +509 -0
- hein-analytical-control/analysis/spec_utils.py +304 -0
- hein-analytical-control/analysis/utils.py +63 -0
- hein-analytical-control/devices/Agilent/__init__.py +3 -0
- hein-analytical-control/devices/Agilent/chemstation.py +290 -0
- hein-analytical-control/devices/Agilent/chromatogram.py +129 -0
- hein-analytical-control/devices/Agilent/hplc.py +436 -0
- hein-analytical-control/devices/Agilent/hplc_param_types.py +141 -0
- hein-analytical-control/devices/Magritek/Spinsolve/__init__.py +0 -0
- hein-analytical-control/devices/Magritek/Spinsolve/commands.py +495 -0
- hein-analytical-control/devices/Magritek/Spinsolve/spectrum.py +822 -0
- hein-analytical-control/devices/Magritek/Spinsolve/spinsolve.py +425 -0
- hein-analytical-control/devices/Magritek/Spinsolve/utils/__init__.py +5 -0
- hein-analytical-control/devices/Magritek/Spinsolve/utils/connection.py +168 -0
- hein-analytical-control/devices/Magritek/Spinsolve/utils/constants.py +8 -0
- hein-analytical-control/devices/Magritek/Spinsolve/utils/exceptions.py +25 -0
- hein-analytical-control/devices/Magritek/Spinsolve/utils/parser.py +340 -0
- hein-analytical-control/devices/Magritek/Spinsolve/utils/shimming.py +55 -0
- hein-analytical-control/devices/Magritek/Spinsolve/utils/spinsolve_logging.py +43 -0
- hein-analytical-control/devices/Magritek/__init__.py +0 -0
- hein-analytical-control/devices/OceanOptics/IR/NIRQuest512.py +90 -0
- hein-analytical-control/devices/OceanOptics/IR/__init__.py +0 -0
- hein-analytical-control/devices/OceanOptics/IR/ir_spectrum.py +191 -0
- hein-analytical-control/devices/OceanOptics/Raman/__init__.py +0 -0
- hein-analytical-control/devices/OceanOptics/Raman/raman_control.py +46 -0
- hein-analytical-control/devices/OceanOptics/Raman/raman_spectrum.py +148 -0
- hein-analytical-control/devices/OceanOptics/UV/QEPro2192.py +90 -0
- hein-analytical-control/devices/OceanOptics/UV/__init__.py +0 -0
- hein-analytical-control/devices/OceanOptics/UV/uv_spectrum.py +227 -0
- hein-analytical-control/devices/OceanOptics/__init__.py +0 -0
- hein-analytical-control/devices/OceanOptics/oceanoptics.py +115 -0
- hein-analytical-control/devices/__init__.py +15 -0
- hein-analytical-control/generated/__init__.py +56 -0
- hein-analytical-control/generated/dad_method.py +367 -0
- hein-analytical-control/generated/pump_method.py +519 -0
- hein_analytical_control/__init__.py +3 -0
- hein_analytical_control/analysis/__init__.py +1 -0
- hein_analytical_control/analysis/base_spectrum.py +509 -0
- hein_analytical_control/analysis/spec_utils.py +304 -0
- hein_analytical_control/analysis/utils.py +63 -0
- hein_analytical_control/devices/Agilent/__init__.py +3 -0
- hein_analytical_control/devices/Agilent/chemstation.py +290 -0
- hein_analytical_control/devices/Agilent/chromatogram.py +129 -0
- hein_analytical_control/devices/Agilent/hplc.py +436 -0
- hein_analytical_control/devices/Agilent/hplc_param_types.py +141 -0
- hein_analytical_control/devices/Magritek/Spinsolve/__init__.py +0 -0
- hein_analytical_control/devices/Magritek/Spinsolve/commands.py +495 -0
- hein_analytical_control/devices/Magritek/Spinsolve/spectrum.py +822 -0
- hein_analytical_control/devices/Magritek/Spinsolve/spinsolve.py +425 -0
- hein_analytical_control/devices/Magritek/Spinsolve/utils/__init__.py +5 -0
- hein_analytical_control/devices/Magritek/Spinsolve/utils/connection.py +168 -0
- hein_analytical_control/devices/Magritek/Spinsolve/utils/constants.py +8 -0
- hein_analytical_control/devices/Magritek/Spinsolve/utils/exceptions.py +25 -0
- hein_analytical_control/devices/Magritek/Spinsolve/utils/parser.py +340 -0
- hein_analytical_control/devices/Magritek/Spinsolve/utils/shimming.py +55 -0
- hein_analytical_control/devices/Magritek/Spinsolve/utils/spinsolve_logging.py +43 -0
- hein_analytical_control/devices/Magritek/__init__.py +0 -0
- hein_analytical_control/devices/OceanOptics/IR/NIRQuest512.py +90 -0
- hein_analytical_control/devices/OceanOptics/IR/__init__.py +0 -0
- hein_analytical_control/devices/OceanOptics/IR/ir_spectrum.py +191 -0
- hein_analytical_control/devices/OceanOptics/Raman/__init__.py +0 -0
- hein_analytical_control/devices/OceanOptics/Raman/raman_control.py +46 -0
- hein_analytical_control/devices/OceanOptics/Raman/raman_spectrum.py +148 -0
- hein_analytical_control/devices/OceanOptics/UV/QEPro2192.py +90 -0
- hein_analytical_control/devices/OceanOptics/UV/__init__.py +0 -0
- hein_analytical_control/devices/OceanOptics/UV/uv_spectrum.py +227 -0
- hein_analytical_control/devices/OceanOptics/__init__.py +0 -0
- hein_analytical_control/devices/OceanOptics/oceanoptics.py +115 -0
- hein_analytical_control/devices/__init__.py +15 -0
- hein_analytical_control/generated/__init__.py +56 -0
- hein_analytical_control/generated/dad_method.py +367 -0
- hein_analytical_control/generated/pump_method.py +519 -0
- pychemstation/__init__.py +3 -0
- pychemstation/analysis/__init__.py +1 -0
- pychemstation/analysis/base_spectrum.py +509 -0
- pychemstation/analysis/spec_utils.py +304 -0
- pychemstation/analysis/utils.py +63 -0
- pychemstation/control/__init__.py +5 -0
- pychemstation/control/chromatogram.py +128 -0
- pychemstation/control/hplc.py +673 -0
- pychemstation/generated/__init__.py +56 -0
- pychemstation/generated/dad_method.py +367 -0
- pychemstation/generated/pump_method.py +519 -0
- pychemstation/utils/__init__.py +2 -0
- pychemstation/utils/chemstation.py +290 -0
- pychemstation/utils/constants.py +15 -0
- pychemstation/utils/hplc_param_types.py +185 -0
- pychemstation-0.4.7.dev1.dist-info/LICENSE +395 -0
- pychemstation-0.4.7.dev1.dist-info/METADATA +102 -0
- pychemstation-0.4.7.dev1.dist-info/RECORD +109 -0
- pychemstation-0.4.7.dev1.dist-info/WHEEL +5 -0
- pychemstation-0.4.7.dev1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,129 @@
|
|
1
|
+
"""Module for HPLC chromatogram data loading and manipulating"""
|
2
|
+
|
3
|
+
import os
|
4
|
+
import logging
|
5
|
+
import time
|
6
|
+
|
7
|
+
import numpy as np
|
8
|
+
|
9
|
+
from .chemstation import CHFile
|
10
|
+
|
11
|
+
from ...analysis import AbstractSpectrum
|
12
|
+
|
13
|
+
# Chemstation data path
|
14
|
+
DATA_DIR = r"C:\Chem32\1\Data"
|
15
|
+
|
16
|
+
|
17
|
+
# standard filenames for spectral data
|
18
|
+
CHANNELS = {"A": "01", "B": "02", "C": "03", "D": "04"}
|
19
|
+
|
20
|
+
ACQUISITION_PARAMETERS = "acq.txt"
|
21
|
+
|
22
|
+
# format used in acquisition parameters
|
23
|
+
TIME_FORMAT = "%Y-%m-%d-%H-%M-%S"
|
24
|
+
|
25
|
+
|
26
|
+
class AgilentHPLCChromatogram(AbstractSpectrum):
|
27
|
+
"""Class for HPLC spectrum (chromatogram) loading and handling."""
|
28
|
+
|
29
|
+
AXIS_MAPPING = {"x": "min", "y": "mAu"}
|
30
|
+
|
31
|
+
INTERNAL_PROPERTIES = {
|
32
|
+
"baseline",
|
33
|
+
"parameters",
|
34
|
+
"data_path",
|
35
|
+
}
|
36
|
+
|
37
|
+
# set of properties to be saved
|
38
|
+
PUBLIC_PROPERTIES = {
|
39
|
+
"x",
|
40
|
+
"y",
|
41
|
+
"peaks",
|
42
|
+
"timestamp",
|
43
|
+
}
|
44
|
+
|
45
|
+
def __init__(self, path=None, autosaving=False):
|
46
|
+
|
47
|
+
if path is not None:
|
48
|
+
os.makedirs(path, exist_ok=True)
|
49
|
+
self.path = path
|
50
|
+
else:
|
51
|
+
self.path = os.path.join(".", "hplc_data")
|
52
|
+
os.makedirs(self.path, exist_ok=True)
|
53
|
+
|
54
|
+
self.logger = logging.getLogger("AgilentHPLCChromatogram")
|
55
|
+
|
56
|
+
super().__init__(path=path, autosaving=autosaving)
|
57
|
+
|
58
|
+
def load_spectrum(self, data_path, channel="A"):
|
59
|
+
"""Loads the spectra from the given folder.
|
60
|
+
|
61
|
+
Args:
|
62
|
+
data_path (str): Path where HPLC data has been saved.
|
63
|
+
"""
|
64
|
+
|
65
|
+
# to avoid dropping parameters when called in parent class
|
66
|
+
if self.x is not None:
|
67
|
+
if self.autosaving:
|
68
|
+
self.save_data(filename=f"{data_path}_{channel}")
|
69
|
+
self._dump()
|
70
|
+
|
71
|
+
# get raw data
|
72
|
+
x, y = self.extract_rawdata(data_path, channel)
|
73
|
+
|
74
|
+
# get timestamp
|
75
|
+
tstr = data_path.split(".")[0].split("_")[-1]
|
76
|
+
timestamp = time.mktime(time.strptime(tstr, TIME_FORMAT))
|
77
|
+
|
78
|
+
# loading all data
|
79
|
+
super().load_spectrum(x, y, timestamp)
|
80
|
+
|
81
|
+
### PUBLIC METHODS TO LOAD RAW DATA ###
|
82
|
+
|
83
|
+
def extract_rawdata(self, experiment_dir: str, channel: str):
|
84
|
+
"""
|
85
|
+
Reads raw data from Chemstation .CH files.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
experiment_dir: .D directory with the .CH files
|
89
|
+
|
90
|
+
Returns:
|
91
|
+
np.array(times), np.array(values) Raw chromatogram data
|
92
|
+
"""
|
93
|
+
filename = os.path.join(experiment_dir, f"DAD1{channel}")
|
94
|
+
npz_file = filename + ".npz"
|
95
|
+
|
96
|
+
if os.path.exists(npz_file):
|
97
|
+
# already processed
|
98
|
+
data = np.load(npz_file)
|
99
|
+
return data["times"], data["values"]
|
100
|
+
else:
|
101
|
+
self.logger.debug("NPZ file not found. First time loading data.")
|
102
|
+
ch_file = filename + ".ch"
|
103
|
+
data = CHFile(ch_file)
|
104
|
+
np.savez_compressed(npz_file, times=data.times, values=data.values)
|
105
|
+
return np.array(data.times), np.array(data.values)
|
106
|
+
|
107
|
+
def extract_peakarea(self, experiment_dir: str):
|
108
|
+
"""
|
109
|
+
Reads processed data from Chemstation report files.
|
110
|
+
|
111
|
+
Args:
|
112
|
+
experiment_dir: .D directory with the report files
|
113
|
+
"""
|
114
|
+
# filename = os.path.join(experiment_dir, f"REPORT{CHANNELS[channel]}.csv")
|
115
|
+
# TODO parse file properly
|
116
|
+
# data = np.genfromtxt(filename, delimiter=',')
|
117
|
+
# return data
|
118
|
+
pass
|
119
|
+
|
120
|
+
def default_processing(self):
|
121
|
+
"""
|
122
|
+
Processes the chromatogram in place.
|
123
|
+
"""
|
124
|
+
# trim first 5 min and last 3 min of run
|
125
|
+
self.trim(5, 25)
|
126
|
+
# parameters found to work best for chromatogram data
|
127
|
+
self.correct_baseline(lmbd=1e5, p=0.0001, n_iter=10)
|
128
|
+
# get all peaks in processed chromatogram
|
129
|
+
self.find_peaks()
|
@@ -0,0 +1,436 @@
|
|
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 .hplc_param_types import Command, HPLCAvailStatus, HPLCMethodParams, HPLCRunningStatus, HPLCErrorStatus, Param, \
|
23
|
+
str_to_status, PType, MethodTimetable, Entry, RegisterFlag
|
24
|
+
from ...generated import PumpMethod, DadMethod
|
25
|
+
|
26
|
+
# maximum command number
|
27
|
+
MAX_CMD_NO = 255
|
28
|
+
|
29
|
+
|
30
|
+
class HPLCController:
|
31
|
+
"""
|
32
|
+
Class to control Agilent HPLC systems via Chemstation Macros.
|
33
|
+
"""
|
34
|
+
|
35
|
+
def __init__(
|
36
|
+
self,
|
37
|
+
comm_dir: str,
|
38
|
+
data_dir: str,
|
39
|
+
method_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
|
+
self.spectra = {
|
71
|
+
"A": AgilentHPLCChromatogram(self.data_dir),
|
72
|
+
"B": AgilentHPLCChromatogram(self.data_dir),
|
73
|
+
"C": AgilentHPLCChromatogram(self.data_dir),
|
74
|
+
"D": AgilentHPLCChromatogram(self.data_dir),
|
75
|
+
}
|
76
|
+
|
77
|
+
self.data_files: list[str] = []
|
78
|
+
|
79
|
+
# Create files for Chemstation to communicate with Python
|
80
|
+
open(self.cmd_file, "a").close()
|
81
|
+
open(self.reply_file, "a").close()
|
82
|
+
|
83
|
+
self.logger = logging.getLogger("hplc_logger")
|
84
|
+
self.logger.addHandler(logging.NullHandler())
|
85
|
+
|
86
|
+
self.reset_cmd_counter()
|
87
|
+
|
88
|
+
self.logger.info("HPLC Controller initialized.")
|
89
|
+
|
90
|
+
def _set_status(self):
|
91
|
+
"""Updates current status of HPLC machine"""
|
92
|
+
self._most_recent_hplc_status = self.status()[0]
|
93
|
+
|
94
|
+
def _check_data_status(self) -> bool:
|
95
|
+
"""Checks if HPLC machine is in an available state, meaning a state that data is not being written.
|
96
|
+
|
97
|
+
:return: whether the HPLC machine is in a safe state to retrieve data back."""
|
98
|
+
old_status = self._most_recent_hplc_status
|
99
|
+
self._set_status()
|
100
|
+
file_exists = os.path.exists(self.data_files[-1]) if len(self.data_files) > 0 else False
|
101
|
+
done_writing_data = isinstance(self._most_recent_hplc_status,
|
102
|
+
HPLCAvailStatus) and old_status != self._most_recent_hplc_status and file_exists
|
103
|
+
return done_writing_data
|
104
|
+
|
105
|
+
def check_hplc_ready_with_data(self) -> bool:
|
106
|
+
"""Checks if ChemStation has finished writing data and can be read back.
|
107
|
+
|
108
|
+
: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
|
109
|
+
|
110
|
+
:return: Return True if data can be read back, else False.
|
111
|
+
"""
|
112
|
+
self._set_status()
|
113
|
+
|
114
|
+
timeout = 10 * 60
|
115
|
+
hplc_run_done = polling.poll(
|
116
|
+
lambda: self._check_data_status(),
|
117
|
+
timeout=timeout,
|
118
|
+
step=30
|
119
|
+
)
|
120
|
+
|
121
|
+
return hplc_run_done
|
122
|
+
|
123
|
+
def _send(self, cmd: str, cmd_no: int, num_attempts=5) -> None:
|
124
|
+
"""Low-level execution primitive. Sends a command string to HPLC.
|
125
|
+
|
126
|
+
:param cmd: string to be sent to HPLC
|
127
|
+
:param cmd_no: Command number
|
128
|
+
:param num_attempts: Number of attempts to send the command before raising exception.
|
129
|
+
:raises IOError: Could not write to command file.
|
130
|
+
"""
|
131
|
+
err = None
|
132
|
+
for _ in range(num_attempts):
|
133
|
+
time.sleep(1)
|
134
|
+
try:
|
135
|
+
with open(self.cmd_file, "w", encoding="utf8") as cmd_file:
|
136
|
+
cmd_file.write(f"{cmd_no} {cmd}")
|
137
|
+
except IOError as e:
|
138
|
+
err = e
|
139
|
+
self.logger.warning("Failed to send command; trying again.")
|
140
|
+
continue
|
141
|
+
else:
|
142
|
+
self.logger.info("Sent command #%d: %s.", cmd_no, cmd)
|
143
|
+
return
|
144
|
+
else:
|
145
|
+
raise IOError(f"Failed to send command #{cmd_no}: {cmd}.") from err
|
146
|
+
|
147
|
+
def _receive(self, cmd_no: int, num_attempts=100) -> str:
|
148
|
+
"""Low-level execution primitive. Recives a response from HPLC.
|
149
|
+
|
150
|
+
:param cmd_no: Command number
|
151
|
+
:param num_attempts: Number of retries to open reply file
|
152
|
+
:raises IOError: Could not read reply file.
|
153
|
+
:return: ChemStation response
|
154
|
+
"""
|
155
|
+
err = None
|
156
|
+
for _ in range(num_attempts):
|
157
|
+
time.sleep(1)
|
158
|
+
|
159
|
+
try:
|
160
|
+
with open(self.reply_file, "r", encoding="utf_16") as reply_file:
|
161
|
+
response = reply_file.read()
|
162
|
+
except OSError as e:
|
163
|
+
err = e
|
164
|
+
self.logger.warning("Failed to read from reply file; trying again.")
|
165
|
+
continue
|
166
|
+
|
167
|
+
try:
|
168
|
+
first_line = response.splitlines()[0]
|
169
|
+
response_no = int(first_line.split()[0])
|
170
|
+
except IndexError as e:
|
171
|
+
err = e
|
172
|
+
self.logger.warning("Malformed response %s; trying again.", response)
|
173
|
+
continue
|
174
|
+
|
175
|
+
# check that response corresponds to sent command
|
176
|
+
if response_no == cmd_no:
|
177
|
+
self.logger.info("Reply: \n%s", response)
|
178
|
+
return response
|
179
|
+
else:
|
180
|
+
self.logger.warning(
|
181
|
+
"Response #: %d != command #: %d; trying again.",
|
182
|
+
response_no,
|
183
|
+
cmd_no,
|
184
|
+
)
|
185
|
+
continue
|
186
|
+
else:
|
187
|
+
raise IOError(f"Failed to receive reply to command #{cmd_no}.") from err
|
188
|
+
|
189
|
+
def send(self, cmd: Union[Command, str]):
|
190
|
+
"""Sends a command to Chemstation.
|
191
|
+
|
192
|
+
:param cmd: Command to be sent to HPLC
|
193
|
+
"""
|
194
|
+
if self.cmd_no == MAX_CMD_NO:
|
195
|
+
self.reset_cmd_counter()
|
196
|
+
|
197
|
+
cmd_to_send: str = cmd.value if isinstance(cmd, Command) else cmd
|
198
|
+
self.cmd_no += 1
|
199
|
+
self._send(cmd_to_send, self.cmd_no)
|
200
|
+
|
201
|
+
def receive(self) -> str:
|
202
|
+
"""Returns messages received in reply file.
|
203
|
+
|
204
|
+
:return: ChemStation response
|
205
|
+
"""
|
206
|
+
return self._receive(self.cmd_no)
|
207
|
+
|
208
|
+
def reset_cmd_counter(self):
|
209
|
+
"""Resets the command counter."""
|
210
|
+
self._send(Command.RESET_COUNTER_CMD.value, cmd_no=MAX_CMD_NO + 1)
|
211
|
+
self._receive(cmd_no=MAX_CMD_NO + 1)
|
212
|
+
self.cmd_no = 0
|
213
|
+
|
214
|
+
self.logger.debug("Reset command counter")
|
215
|
+
|
216
|
+
def sleep(self, seconds: int):
|
217
|
+
"""Tells the HPLC to wait for a specified number of seconds.
|
218
|
+
|
219
|
+
:param seconds: number of seconds to wait
|
220
|
+
"""
|
221
|
+
self.send(Command.SLEEP_CMD.value.format(seconds=seconds))
|
222
|
+
self.logger.debug("Sleep command sent.")
|
223
|
+
|
224
|
+
def standby(self):
|
225
|
+
"""Switches all modules in standby mode. All lamps and pumps are switched off."""
|
226
|
+
self.send(Command.STANDBY_CMD)
|
227
|
+
self.logger.debug("Standby command sent.")
|
228
|
+
|
229
|
+
def preprun(self):
|
230
|
+
""" Prepares all modules for run. All lamps and pumps are switched on."""
|
231
|
+
self.send(Command.PREPRUN_CMD)
|
232
|
+
self.logger.debug("PrepRun command sent.")
|
233
|
+
|
234
|
+
def status(self) -> list[Union[HPLCRunningStatus, HPLCAvailStatus, HPLCErrorStatus]]:
|
235
|
+
"""Get device status(es).
|
236
|
+
|
237
|
+
:return: list of ChemStation's current status
|
238
|
+
"""
|
239
|
+
self.send(Command.GET_STATUS_CMD)
|
240
|
+
time.sleep(1)
|
241
|
+
|
242
|
+
try:
|
243
|
+
parsed_response = self.receive().splitlines()[1].split()[1:]
|
244
|
+
except IOError:
|
245
|
+
return [HPLCErrorStatus.NORESPONSE]
|
246
|
+
except IndexError:
|
247
|
+
return [HPLCErrorStatus.MALFORMED]
|
248
|
+
recieved_status = [str_to_status(res) for res in parsed_response]
|
249
|
+
self._most_recent_hplc_status = recieved_status[0]
|
250
|
+
return recieved_status
|
251
|
+
|
252
|
+
def stop_macro(self):
|
253
|
+
"""Stops Macro execution. Connection will be lost."""
|
254
|
+
self.send(Command.STOP_MACRO_CMD)
|
255
|
+
|
256
|
+
def update_method(self):
|
257
|
+
pass
|
258
|
+
|
259
|
+
def edit_method_timetable(self):
|
260
|
+
pass
|
261
|
+
|
262
|
+
def _update_method_timetable(self):
|
263
|
+
pass
|
264
|
+
|
265
|
+
def _update_method_param(self, method_param: Param):
|
266
|
+
"""Change a method parameter.
|
267
|
+
|
268
|
+
:param method_param: a parameter to update for currently loaded method
|
269
|
+
"""
|
270
|
+
|
271
|
+
setting_command = "SetObjHdrVal" if method_param.ptype == PType.NUM else "SetObjHdrText"
|
272
|
+
if isinstance(method_param.chemstation_key, list):
|
273
|
+
for register in method_param.chemstation_key:
|
274
|
+
self.send(
|
275
|
+
f'{setting_command} RCPMP1Method[1], {register}, {method_param.val}'
|
276
|
+
)
|
277
|
+
else:
|
278
|
+
self.send(
|
279
|
+
f'{setting_command} RCPMP1Method[1], {method_param.chemstation_key}, {method_param.val}'
|
280
|
+
)
|
281
|
+
time.sleep(2)
|
282
|
+
|
283
|
+
def desired_method_already_loaded(self, method_name: str) -> bool:
|
284
|
+
"""Checks if a given method is already loaded into Chemstation. Method name does not need the ".M" extension.
|
285
|
+
|
286
|
+
:param method_name: a Chemstation method
|
287
|
+
:return: True if method is already loaded
|
288
|
+
"""
|
289
|
+
self.send(Command.GET_METHOD_CMD)
|
290
|
+
parsed_response = self.receive().splitlines()[1].split()[1:][0]
|
291
|
+
return method_name in parsed_response
|
292
|
+
|
293
|
+
def switch_method(self, method_name: str):
|
294
|
+
"""Allows the user to switch between pre-programmed methods. No need to append '.M'
|
295
|
+
to the end of the method name. For example. for the method named 'General-Poroshell.M',
|
296
|
+
only 'General-Poroshell' is needed.
|
297
|
+
|
298
|
+
:param method_name: any available method in Chemstation method directory
|
299
|
+
:raise IndexError: Response did not have expected format. Try again.
|
300
|
+
:raise AssertionError: The desired method is not selected. Try again.
|
301
|
+
"""
|
302
|
+
self.send(
|
303
|
+
Command.SWITCH_METHOD_CMD.value.format(method_dir=self.method_dir, method_name=method_name)
|
304
|
+
)
|
305
|
+
|
306
|
+
time.sleep(2)
|
307
|
+
self.send(Command.GET_METHOD_CMD)
|
308
|
+
time.sleep(2)
|
309
|
+
# check that method switched
|
310
|
+
for _ in range(10):
|
311
|
+
try:
|
312
|
+
parsed_response = self.receive().splitlines()[1].split()[1:][0]
|
313
|
+
break
|
314
|
+
except IndexError:
|
315
|
+
self.logger.debug("Malformed response. Trying again.")
|
316
|
+
continue
|
317
|
+
|
318
|
+
assert parsed_response == f"{method_name}.M", "Switching Methods failed."
|
319
|
+
|
320
|
+
def load_method_details(self, method_name: str) -> MethodTimetable:
|
321
|
+
"""Retrieve method details of an existing method. Don't need to append ".M" to the end. This assumes the
|
322
|
+
organic modifier is in Channel B and that Channel A contains the aq layer. Additionally, assumes
|
323
|
+
only two solvents are being used.
|
324
|
+
|
325
|
+
:param method_name: name of method to load details of
|
326
|
+
:raises FileNotFoundError: Method does not exist
|
327
|
+
:return: method details
|
328
|
+
"""
|
329
|
+
method_path = os.path.join(self.method_dir, "AgilentPumpDriver1.RapidControl.MethodXML.xml")
|
330
|
+
dad_path = os.path.join(self.method_dir, "Agilent1200erDadDriver1.RapidControl.MethodXML.xml")
|
331
|
+
|
332
|
+
if os.path.exists(os.path.join(self.method_dir, f"{method_name}.M")):
|
333
|
+
parser = XmlParser()
|
334
|
+
method = parser.parse(method_path, PumpMethod)
|
335
|
+
dad = parser.parse(dad_path, DadMethod)
|
336
|
+
|
337
|
+
organic_modifier = None
|
338
|
+
aq_modifier = None
|
339
|
+
|
340
|
+
if len(method.solvent_composition.solvent_element) == 2:
|
341
|
+
for solvent in method.solvent_composition.solvent_element:
|
342
|
+
if solvent.channel == "Channel_A":
|
343
|
+
aq_modifier = solvent
|
344
|
+
elif solvent.channel == "Channel_B":
|
345
|
+
organic_modifier = solvent
|
346
|
+
|
347
|
+
return MethodTimetable(
|
348
|
+
first_row=HPLCMethodParams(
|
349
|
+
organic_modifier=Param(val=organic_modifier.percentage,
|
350
|
+
chemstation_key=RegisterFlag.SOLVENT_B_COMPOSITION,
|
351
|
+
ptype=PType.NUM),
|
352
|
+
flow=Param(val=method.flow,
|
353
|
+
chemstation_key=RegisterFlag.FLOW,
|
354
|
+
ptype=PType.NUM),
|
355
|
+
maximum_run_time=Param(val=method,
|
356
|
+
chemstation_key=RegisterFlag.MAX_TIME,
|
357
|
+
ptype=PType.NUM),
|
358
|
+
temperature=Param(val=None,
|
359
|
+
chemstation_key=[RegisterFlag.COLUMN_OVEN_TEMP1,
|
360
|
+
RegisterFlag.COLUMN_OVEN_TEMP2],
|
361
|
+
ptype=PType.NUM),
|
362
|
+
inj_vol=Param(val=None,
|
363
|
+
chemstation_key=None,
|
364
|
+
ptype=PType.NUM),
|
365
|
+
equ_time=Param(val=None,
|
366
|
+
chemstation_key=None,
|
367
|
+
ptype=PType.NUM)),
|
368
|
+
subsequent_rows=[
|
369
|
+
Entry(
|
370
|
+
start_time=tte.time,
|
371
|
+
organic_modifer=tte.percent_b,
|
372
|
+
flow=method.flow
|
373
|
+
) for tte in method.timetable.timetable_entry
|
374
|
+
],
|
375
|
+
dad_wavelengthes=dad.signals.signal,
|
376
|
+
organic_modifier=organic_modifier,
|
377
|
+
modifier_a=aq_modifier
|
378
|
+
)
|
379
|
+
else:
|
380
|
+
raise FileNotFoundError
|
381
|
+
|
382
|
+
def lamp_on(self):
|
383
|
+
"""Turns the UV lamp on."""
|
384
|
+
self.send(Command.LAMP_ON_CMD)
|
385
|
+
|
386
|
+
def lamp_off(self):
|
387
|
+
"""Turns the UV lamp off."""
|
388
|
+
self.send(Command.LAMP_OFF_CMD)
|
389
|
+
|
390
|
+
def pump_on(self):
|
391
|
+
"""Turns on the pump on."""
|
392
|
+
self.send(Command.PUMP_ON_CMD)
|
393
|
+
|
394
|
+
def pump_off(self):
|
395
|
+
"""Turns the pump off."""
|
396
|
+
self.send(Command.PUMP_OFF_CMD)
|
397
|
+
|
398
|
+
def start_method(self):
|
399
|
+
"""Starts and executes currently loaded method to run according to Run Time Checklist. Device must be ready."""
|
400
|
+
self.send(Command.START_METHOD_CMD)
|
401
|
+
|
402
|
+
def run_method(self, experiment_name: str):
|
403
|
+
"""This is the preferred method to trigger a run.
|
404
|
+
Starts the currently selected method, storing data
|
405
|
+
under the <data_dir>/<experiment_name>.D folder.
|
406
|
+
The should <experiment_name> end with a timestamp in the '%Y-%m-%d-%H-%M' format.
|
407
|
+
Device must be ready.
|
408
|
+
|
409
|
+
:param experiment_name: Name of the experiment
|
410
|
+
"""
|
411
|
+
timestamp = time.strftime(TIME_FORMAT)
|
412
|
+
|
413
|
+
self.send(
|
414
|
+
Command.RUN_METHOD_CMD.value.format(
|
415
|
+
data_dir=self.data_dir, experiment_name=experiment_name, timestamp=timestamp
|
416
|
+
)
|
417
|
+
)
|
418
|
+
|
419
|
+
folder_name = f"{experiment_name}_{timestamp}.D"
|
420
|
+
self.data_files.append(os.path.join(self.data_dir, folder_name))
|
421
|
+
self.logger.info("Started HPLC run: %s.", folder_name)
|
422
|
+
|
423
|
+
def stop_method(self):
|
424
|
+
"""Stops the run. A dialog window will pop up and manual intervention may be required."""
|
425
|
+
self.send(Command.STOP_METHOD_CMD)
|
426
|
+
|
427
|
+
def get_spectrum(self):
|
428
|
+
""" Load last chromatogram for any channel in spectra dictionary."""
|
429
|
+
last_file = self.data_files[-1] if len(self.data_files) > 0 else None
|
430
|
+
|
431
|
+
if last_file is None:
|
432
|
+
raise IndexError
|
433
|
+
|
434
|
+
for channel, spec in self.spectra.items():
|
435
|
+
spec.load_spectrum(data_path=last_file, channel=channel)
|
436
|
+
self.logger.info("%s chromatogram loaded.", channel)
|
@@ -0,0 +1,141 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from enum import Enum
|
3
|
+
from typing import Union, Any
|
4
|
+
|
5
|
+
from ...generated import SolventElement, Signal
|
6
|
+
|
7
|
+
|
8
|
+
# Commands sent to the Chemstation Macro
|
9
|
+
# See https://www.agilent.com/cs/library/usermanuals/Public/MACROS.PDF
|
10
|
+
class Command(Enum):
|
11
|
+
def __str__(self):
|
12
|
+
return '%s' % self.value
|
13
|
+
|
14
|
+
RESET_COUNTER_CMD = "last_cmd_no = 0"
|
15
|
+
GET_STATUS_CMD = "response$ = AcqStatus$"
|
16
|
+
SLEEP_CMD = "Sleep {seconds}"
|
17
|
+
STANDBY_CMD = "Standby"
|
18
|
+
STOP_MACRO_CMD = "Stop"
|
19
|
+
PREPRUN_CMD = "PrepRun"
|
20
|
+
LAMP_ON_CMD = "LampAll ON"
|
21
|
+
LAMP_OFF_CMD = "LampAll OFF"
|
22
|
+
PUMP_ON_CMD = "PumpAll ON"
|
23
|
+
PUMP_OFF_CMD = "PumpAll OFF"
|
24
|
+
GET_METHOD_CMD = "response$ = _MethFile$"
|
25
|
+
SWITCH_METHOD_CMD = 'LoadMethod "{method_dir}", "{method_name}.M"'
|
26
|
+
START_METHOD_CMD = "StartMethod"
|
27
|
+
RUN_METHOD_CMD = 'RunMethod "{data_dir}",, "{experiment_name}_{timestamp}"'
|
28
|
+
STOP_METHOD_CMD = "StopMethod"
|
29
|
+
UPDATE_METHOD_CMD = 'UpdateMethod'
|
30
|
+
|
31
|
+
|
32
|
+
class RegisterFlag(Enum):
|
33
|
+
def __str__(self):
|
34
|
+
return '%s' % self.value
|
35
|
+
|
36
|
+
SOLVENT_A_COMPOSITION = "PumpChannel_CompositionPercentage"
|
37
|
+
SOLVENT_B_COMPOSITION = "PumpChannel2_CompositionPercentage"
|
38
|
+
SOLVENT_C_COMPOSITION = "PumpChannel3_CompositionPercentage"
|
39
|
+
SOLVENT_D_COMPOSITION = "PumpChannel4_CompositionPercentage"
|
40
|
+
FLOW = "Flow"
|
41
|
+
MAX_TIME = "StopTime_Time"
|
42
|
+
COLUMN_OVEN_TEMP1 = "TemperatureControl_Temperature"
|
43
|
+
COLUMN_OVEN_TEMP2 = "TemperatureControl2_Temperature"
|
44
|
+
STOPTIME_MODE = "StopTime_Mode"
|
45
|
+
POSTIME_MODE = "PostTime_Mode"
|
46
|
+
|
47
|
+
|
48
|
+
class MethodTableOperation(Enum):
|
49
|
+
def __str__(self):
|
50
|
+
return '%s' % self.value
|
51
|
+
|
52
|
+
DELETE_TABLE = 'DelTab RCPMP1Method[1], "Timetable"'
|
53
|
+
CREATE_TABLE = 'NewTab RCPMP1Method[1], "Timetable"'
|
54
|
+
NEW_ROW = 'InsTabRow RCPMP1Method[1], "Timetable"'
|
55
|
+
NEW_COLUMN = ""
|
56
|
+
DOWNLOAD_TABLE = ""
|
57
|
+
|
58
|
+
|
59
|
+
class PType(Enum):
|
60
|
+
STR = "str"
|
61
|
+
NUM = "num"
|
62
|
+
|
63
|
+
|
64
|
+
@dataclass
|
65
|
+
class Param:
|
66
|
+
ptype: PType
|
67
|
+
val: Union[float, int, str, Any]
|
68
|
+
chemstation_key: Union[RegisterFlag, list[RegisterFlag]]
|
69
|
+
|
70
|
+
|
71
|
+
@dataclass
|
72
|
+
class HPLCMethodParams:
|
73
|
+
organic_modifier: Param
|
74
|
+
flow: Param
|
75
|
+
temperature: Param
|
76
|
+
inj_vol: Param
|
77
|
+
equ_time: Param
|
78
|
+
maximum_run_time: Param
|
79
|
+
|
80
|
+
|
81
|
+
@dataclass
|
82
|
+
class Entry:
|
83
|
+
start_time: float
|
84
|
+
organic_modifer: float
|
85
|
+
flow: float
|
86
|
+
|
87
|
+
|
88
|
+
@dataclass
|
89
|
+
class MethodTimetable:
|
90
|
+
first_row: HPLCMethodParams
|
91
|
+
subsequent_rows: list[Entry]
|
92
|
+
dad_wavelengthes: list[Signal]
|
93
|
+
organic_modifier: SolventElement
|
94
|
+
modifier_a: SolventElement
|
95
|
+
|
96
|
+
|
97
|
+
class HPLCRunningStatus(Enum):
|
98
|
+
@classmethod
|
99
|
+
def has_member_key(cls, key):
|
100
|
+
return key in cls.__members__
|
101
|
+
|
102
|
+
INJECTING = "INJECTING"
|
103
|
+
PREPARING = "PREPARING"
|
104
|
+
RUN = "RUN"
|
105
|
+
NOTREADY = "NOTREADY"
|
106
|
+
POSTRUN = "POSTRUN"
|
107
|
+
RAWDATA = "RAWDATA"
|
108
|
+
INITIALIZING = "INITIALIZING"
|
109
|
+
NOMODULE = "NOMODULE"
|
110
|
+
|
111
|
+
|
112
|
+
class HPLCAvailStatus(Enum):
|
113
|
+
@classmethod
|
114
|
+
def has_member_key(cls, key):
|
115
|
+
return key in cls.__members__
|
116
|
+
|
117
|
+
PRERUN = "PRERUN"
|
118
|
+
OFFLINE = "OFFLINE"
|
119
|
+
STANDBY = "STANDBY"
|
120
|
+
|
121
|
+
|
122
|
+
class HPLCErrorStatus(Enum):
|
123
|
+
|
124
|
+
@classmethod
|
125
|
+
def has_member_key(cls, key):
|
126
|
+
return key in cls.__members__
|
127
|
+
|
128
|
+
ERROR = "ERROR"
|
129
|
+
BREAK = "BREAK"
|
130
|
+
NORESPONSE = "NORESPONSE"
|
131
|
+
MALFORMED = "MALFORMED"
|
132
|
+
|
133
|
+
|
134
|
+
def str_to_status(status: str) -> Union[HPLCAvailStatus, HPLCErrorStatus, HPLCRunningStatus]:
|
135
|
+
if HPLCErrorStatus.has_member_key(status):
|
136
|
+
return HPLCErrorStatus[status]
|
137
|
+
if HPLCRunningStatus.has_member_key(status):
|
138
|
+
return HPLCRunningStatus[status]
|
139
|
+
if HPLCAvailStatus.has_member_key(status):
|
140
|
+
return HPLCAvailStatus[status]
|
141
|
+
raise KeyError(status)
|