pychemstation 0.8.3__py3-none-any.whl → 0.9.0__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.
- pychemstation/__init__.py +1 -1
- pychemstation/analysis/__init__.py +4 -1
- pychemstation/analysis/base_spectrum.py +4 -4
- pychemstation/{utils → analysis}/chromatogram.py +4 -7
- pychemstation/analysis/process_report.py +121 -74
- pychemstation/control/README.md +22 -46
- pychemstation/control/__init__.py +5 -0
- pychemstation/control/controllers/__init__.py +2 -0
- pychemstation/control/controllers/comm.py +39 -18
- pychemstation/control/controllers/devices/device.py +27 -14
- pychemstation/control/controllers/devices/injector.py +33 -89
- pychemstation/control/controllers/tables/method.py +266 -111
- pychemstation/control/controllers/tables/ms.py +7 -4
- pychemstation/control/controllers/tables/sequence.py +171 -82
- pychemstation/control/controllers/tables/table.py +192 -116
- pychemstation/control/hplc.py +117 -83
- pychemstation/generated/__init__.py +0 -2
- pychemstation/generated/dad_method.py +1 -1
- pychemstation/generated/pump_method.py +15 -19
- pychemstation/utils/injector_types.py +1 -1
- pychemstation/utils/macro.py +12 -11
- pychemstation/utils/method_types.py +3 -2
- pychemstation/{analysis/utils.py → utils/num_utils.py} +2 -2
- pychemstation/utils/parsing.py +1 -11
- pychemstation/utils/sequence_types.py +4 -5
- pychemstation/{analysis → utils}/spec_utils.py +1 -2
- pychemstation/utils/table_types.py +10 -9
- pychemstation/utils/tray_types.py +48 -38
- {pychemstation-0.8.3.dist-info → pychemstation-0.9.0.dist-info}/METADATA +63 -24
- pychemstation-0.9.0.dist-info/RECORD +37 -0
- pychemstation-0.8.3.dist-info/RECORD +0 -37
- {pychemstation-0.8.3.dist-info → pychemstation-0.9.0.dist-info}/WHEEL +0 -0
- {pychemstation-0.8.3.dist-info → pychemstation-0.9.0.dist-info}/licenses/LICENSE +0 -0
pychemstation/__init__.py
CHANGED
@@ -6,12 +6,12 @@ from abc import ABC, abstractmethod
|
|
6
6
|
import matplotlib.pyplot as plt
|
7
7
|
import numpy as np
|
8
8
|
from scipy import (
|
9
|
-
sparse,
|
10
|
-
signal,
|
11
9
|
integrate,
|
10
|
+
signal,
|
11
|
+
sparse,
|
12
12
|
)
|
13
13
|
|
14
|
-
from .
|
14
|
+
from ..utils.num_utils import find_nearest_value_index, interpolate_to_index
|
15
15
|
|
16
16
|
|
17
17
|
class AbstractSpectrum(ABC):
|
@@ -249,7 +249,7 @@ class AbstractSpectrum(ABC):
|
|
249
249
|
os.makedirs(path, exist_ok=True)
|
250
250
|
fig.savefig(os.path.join(path, f"{filename}.png"), dpi=150)
|
251
251
|
|
252
|
-
def find_peaks(self, threshold=1, min_width
|
252
|
+
def find_peaks(self, threshold=1, min_width=0.1, min_dist=None, area=None):
|
253
253
|
"""Finds all peaks above the threshold with at least min_width width.
|
254
254
|
|
255
255
|
Args:
|
@@ -6,16 +6,14 @@ from dataclasses import dataclass
|
|
6
6
|
|
7
7
|
import numpy as np
|
8
8
|
|
9
|
-
from .parsing import CHFile
|
10
|
-
from ..analysis import AbstractSpectrum
|
11
9
|
|
12
|
-
|
13
|
-
|
10
|
+
from ..utils.parsing import CHFile
|
11
|
+
from ..analysis.base_spectrum import AbstractSpectrum
|
14
12
|
|
15
13
|
ACQUISITION_PARAMETERS = "acq.txt"
|
16
14
|
|
17
15
|
# format used in acquisition parameters
|
18
|
-
TIME_FORMAT = "%Y-%m-%d
|
16
|
+
TIME_FORMAT = "%Y-%m-%d %H-%M-%S"
|
19
17
|
SEQUENCE_TIME_FORMAT = "%Y-%m-%d %H-%M"
|
20
18
|
|
21
19
|
|
@@ -39,12 +37,11 @@ class AgilentHPLCChromatogram(AbstractSpectrum):
|
|
39
37
|
}
|
40
38
|
|
41
39
|
def __init__(self, path=None, autosaving=False):
|
42
|
-
|
43
40
|
if path is not None:
|
44
41
|
os.makedirs(path, exist_ok=True)
|
45
42
|
self.path = path
|
46
43
|
else:
|
47
|
-
self.path = os.path.join("
|
44
|
+
self.path = os.path.join("../utils", "hplc_data")
|
48
45
|
os.makedirs(self.path, exist_ok=True)
|
49
46
|
|
50
47
|
super().__init__(path=path, autosaving=autosaving)
|
@@ -4,26 +4,32 @@ import re
|
|
4
4
|
from abc import abstractmethod
|
5
5
|
from dataclasses import dataclass
|
6
6
|
from enum import Enum
|
7
|
-
from typing import
|
7
|
+
from typing import AnyStr, Dict, List, Optional, Pattern
|
8
8
|
|
9
9
|
import pandas as pd
|
10
|
-
from aghplctools.ingestion.text import
|
11
|
-
|
12
|
-
|
10
|
+
from aghplctools.ingestion.text import (
|
11
|
+
_area_report_re,
|
12
|
+
_header_block_re,
|
13
|
+
_no_peaks_re,
|
14
|
+
_signal_info_re,
|
15
|
+
_signal_table_re,
|
16
|
+
chunk_string,
|
17
|
+
)
|
18
|
+
from result import Err, Ok, Result
|
13
19
|
|
14
|
-
from
|
15
|
-
from
|
20
|
+
from ..analysis.chromatogram import AgilentHPLCChromatogram
|
21
|
+
from ..utils.tray_types import FiftyFourVialPlate, Tray
|
16
22
|
|
17
23
|
|
18
24
|
@dataclass
|
19
25
|
class AgilentPeak:
|
26
|
+
peak_number: Optional[int]
|
20
27
|
retention_time: float
|
28
|
+
peak_type: Optional[str]
|
21
29
|
width: float
|
22
30
|
area: float
|
23
31
|
height: float
|
24
|
-
|
25
|
-
peak_type: Optional[str]
|
26
|
-
height_percent: Optional[float]
|
32
|
+
area_percent: Optional[float]
|
27
33
|
|
28
34
|
|
29
35
|
@dataclass
|
@@ -67,12 +73,14 @@ class CSVProcessor(ReportProcessor):
|
|
67
73
|
"""
|
68
74
|
Method to parse details from CSV report.
|
69
75
|
|
70
|
-
:
|
76
|
+
:return: subset of complete report details, specifically the sample location, solvents in pumps,
|
71
77
|
and list of peaks at each wavelength channel.
|
72
78
|
"""
|
73
|
-
labels = os.path.join(self.path,
|
79
|
+
labels = os.path.join(self.path, "REPORT00.CSV")
|
74
80
|
if os.path.exists(labels):
|
75
|
-
df_labels: Dict[int, Dict[int:
|
81
|
+
df_labels: Dict[int, Dict[int:AnyStr]] = pd.read_csv(
|
82
|
+
labels, encoding="utf-16", header=None
|
83
|
+
).to_dict()
|
76
84
|
vial_location = []
|
77
85
|
signals = {}
|
78
86
|
solvents = {}
|
@@ -85,18 +93,33 @@ class CSVProcessor(ReportProcessor):
|
|
85
93
|
elif val == "Number of Signals":
|
86
94
|
num_signals = int(df_labels[1][pos])
|
87
95
|
for s in range(1, num_signals + 1):
|
88
|
-
df = pd.read_csv(
|
89
|
-
|
96
|
+
df = pd.read_csv(
|
97
|
+
os.path.join(self.path, f"REPORT0{s}.CSV"),
|
98
|
+
encoding="utf-16",
|
99
|
+
header=None,
|
100
|
+
)
|
90
101
|
peaks = df.apply(lambda row: AgilentPeak(*row), axis=1)
|
91
|
-
|
92
|
-
|
102
|
+
try:
|
103
|
+
wavelength = df_labels[1][pos + s].partition(",4 Ref=off")[0][
|
104
|
+
-3:
|
105
|
+
]
|
106
|
+
wavelength = int(wavelength)
|
107
|
+
signals[wavelength] = list(peaks)
|
108
|
+
except (IndexError, ValueError):
|
109
|
+
# TODO: Ask about the MS signals
|
110
|
+
pass
|
93
111
|
break
|
94
112
|
|
95
|
-
return Ok(
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
113
|
+
return Ok(
|
114
|
+
AgilentReport(
|
115
|
+
signals=[
|
116
|
+
Signals(wavelength=w, peaks=s, data=None)
|
117
|
+
for w, s in signals.items()
|
118
|
+
],
|
119
|
+
vial_location=FiftyFourVialPlate.from_int(int(vial_location)),
|
120
|
+
solvents=solvents,
|
121
|
+
)
|
122
|
+
)
|
100
123
|
|
101
124
|
return Err("No report found")
|
102
125
|
|
@@ -105,34 +128,39 @@ class TXTProcessor(ReportProcessor):
|
|
105
128
|
"""
|
106
129
|
Regex matches for column and unit combinations, courtesy of Veronica Lai.
|
107
130
|
"""
|
131
|
+
|
108
132
|
_column_re_dictionary = {
|
109
|
-
|
110
|
-
|
133
|
+
"Peak": { # peak index
|
134
|
+
"#": "[ ]+(?P<Peak>[\d]+)", # number
|
111
135
|
},
|
112
|
-
|
113
|
-
|
136
|
+
"RetTime": { # retention time
|
137
|
+
"[min]": "(?P<RetTime>[\d]+.[\d]+)", # minutes
|
114
138
|
},
|
115
|
-
|
116
|
-
|
139
|
+
"Type": { # peak type
|
140
|
+
"": "(?P<Type>[A-Z]{1,3}(?: [A-Z]{1,2})*)", # todo this is different from <4.8.8 aghplc tools
|
117
141
|
},
|
118
|
-
|
119
|
-
|
142
|
+
"Width": { # peak width
|
143
|
+
"[min]": "(?P<Width>[\d]+.[\d]+[e+-]*[\d]+)",
|
120
144
|
},
|
121
|
-
|
122
|
-
|
123
|
-
|
145
|
+
"Area": { # peak area
|
146
|
+
"[mAU*s]": "(?P<Area>[\d]+.[\d]+[e+-]*[\d]+)", # area units
|
147
|
+
"%": "(?P<percent>[\d]+.[\d]+[e+-]*[\d]+)", # percent
|
124
148
|
},
|
125
|
-
|
126
|
-
|
149
|
+
"Height": { # peak height
|
150
|
+
"[mAU]": "(?P<Height>[\d]+.[\d]+[e+-]*[\d]+)",
|
127
151
|
},
|
128
|
-
|
129
|
-
|
152
|
+
"Name": {
|
153
|
+
"": "(?P<Name>[^\s]+(?:\s[^\s]+)*)", # peak name
|
130
154
|
},
|
131
155
|
}
|
132
156
|
|
133
|
-
def __init__(
|
134
|
-
|
135
|
-
|
157
|
+
def __init__(
|
158
|
+
self,
|
159
|
+
path: str,
|
160
|
+
min_ret_time: int = 0,
|
161
|
+
max_ret_time: int = 999,
|
162
|
+
target_wavelength_range: List[int] = range(200, 300),
|
163
|
+
):
|
136
164
|
"""
|
137
165
|
Class to process reports in CSV form.
|
138
166
|
|
@@ -149,16 +177,17 @@ class TXTProcessor(ReportProcessor):
|
|
149
177
|
def process_report(self) -> Result[AgilentReport, AnyStr]:
|
150
178
|
"""
|
151
179
|
Method to parse details from CSV report.
|
152
|
-
|
153
|
-
:returns: subset of complete report details, specifically the sample location, solvents in pumps,
|
154
|
-
and list of peaks at each wavelength channel.
|
155
|
-
|
156
180
|
If you want more functionality, use `aghplctools`.
|
157
181
|
`from aghplctools.ingestion.text import pull_hplc_area_from_txt`
|
158
182
|
`signals = pull_hplc_area_from_txt(file_path)`
|
183
|
+
|
184
|
+
:return: subset of complete report details, specifically the sample location, solvents in pumps,
|
185
|
+
and list of peaks at each wavelength channel.
|
159
186
|
"""
|
160
187
|
|
161
|
-
with open(
|
188
|
+
with open(
|
189
|
+
os.path.join(self.path, "REPORT.TXT"), "r", encoding="utf-16"
|
190
|
+
) as openfile:
|
162
191
|
text = openfile.read()
|
163
192
|
|
164
193
|
try:
|
@@ -166,25 +195,33 @@ class TXTProcessor(ReportProcessor):
|
|
166
195
|
except ValueError as e:
|
167
196
|
return Err("No peaks found: " + str(e))
|
168
197
|
|
169
|
-
signals = {
|
198
|
+
signals = {
|
199
|
+
key: signals[key] for key in self.target_wavelength_range if key in signals
|
200
|
+
}
|
170
201
|
|
171
202
|
parsed_signals = []
|
172
203
|
for wavelength, wavelength_dict in signals.items():
|
173
|
-
current_wavelength_signals = Signals(
|
204
|
+
current_wavelength_signals = Signals(
|
205
|
+
wavelength=int(wavelength), peaks=[], data=None
|
206
|
+
)
|
174
207
|
for ret_time, ret_time_dict in wavelength_dict.items():
|
175
208
|
if self.min_ret_time <= ret_time <= self.max_ret_time:
|
176
|
-
current_wavelength_signals.peaks.append(
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
209
|
+
current_wavelength_signals.peaks.append(
|
210
|
+
AgilentPeak(
|
211
|
+
retention_time=ret_time,
|
212
|
+
area=ret_time_dict["Area"],
|
213
|
+
width=ret_time_dict["Width"],
|
214
|
+
height=ret_time_dict["Height"],
|
215
|
+
peak_number=None,
|
216
|
+
peak_type=ret_time_dict["Type"],
|
217
|
+
area_percent=None,
|
218
|
+
)
|
219
|
+
)
|
183
220
|
parsed_signals.append(current_wavelength_signals)
|
184
221
|
|
185
|
-
return Ok(
|
186
|
-
|
187
|
-
|
222
|
+
return Ok(
|
223
|
+
AgilentReport(vial_location=None, solvents=None, signals=parsed_signals)
|
224
|
+
)
|
188
225
|
|
189
226
|
def parse_area_report(self, report_text: str) -> Dict:
|
190
227
|
"""
|
@@ -200,7 +237,7 @@ class TXTProcessor(ReportProcessor):
|
|
200
237
|
should be able to use the `parse_area_report` method of aghplctools v4.8.8
|
201
238
|
"""
|
202
239
|
if re.search(_no_peaks_re, report_text): # There are no peaks in Report.txt
|
203
|
-
raise ValueError(
|
240
|
+
raise ValueError("No peaks found in Report.txt")
|
204
241
|
blocks = _header_block_re.split(report_text)
|
205
242
|
signals = {} # output dictionary
|
206
243
|
for ind, block in enumerate(blocks):
|
@@ -213,23 +250,28 @@ class TXTProcessor(ReportProcessor):
|
|
213
250
|
si = _signal_info_re.match(table)
|
214
251
|
if si is not None:
|
215
252
|
# some error state (e.g. 'not found')
|
216
|
-
if si.group(
|
253
|
+
if si.group("error") != "":
|
217
254
|
continue
|
218
|
-
wavelength = float(si.group(
|
255
|
+
wavelength = float(si.group("wavelength"))
|
219
256
|
if wavelength in signals:
|
220
257
|
# placeholder error raise just in case (this probably won't happen)
|
221
258
|
raise KeyError(
|
222
|
-
f
|
259
|
+
f"The wavelength {float(si.group('wavelength'))} is already in the signals dictionary"
|
260
|
+
)
|
223
261
|
signals[wavelength] = {}
|
224
262
|
# build peak regex
|
225
263
|
peak_re = self.build_peak_regex(table)
|
226
|
-
if
|
264
|
+
if (
|
265
|
+
peak_re is None
|
266
|
+
): # if there are no columns (empty table), continue
|
227
267
|
continue
|
228
|
-
for line in table.split(
|
268
|
+
for line in table.split("\n"):
|
229
269
|
peak = peak_re.match(line)
|
230
270
|
if peak is not None:
|
231
|
-
signals[wavelength][float(peak.group(
|
232
|
-
current = signals[wavelength][
|
271
|
+
signals[wavelength][float(peak.group("RetTime"))] = {}
|
272
|
+
current = signals[wavelength][
|
273
|
+
float(peak.group("RetTime"))
|
274
|
+
]
|
233
275
|
for key in self._column_re_dictionary:
|
234
276
|
if key in peak.re.groupindex:
|
235
277
|
try: # try float conversion, otherwise continue
|
@@ -248,30 +290,35 @@ class TXTProcessor(ReportProcessor):
|
|
248
290
|
:param signal_table: block of lines associated with an area table
|
249
291
|
:return: peak line regex object (<=3.6 _sre.SRE_PATTERN, >=3.7 re.Pattern)
|
250
292
|
"""
|
251
|
-
split_table = signal_table.split(
|
293
|
+
split_table = signal_table.split("\n")
|
252
294
|
if len(split_table) <= 4: # catch peak table with no values
|
253
295
|
return None
|
254
296
|
# todo verify that these indicies are always true
|
255
297
|
column_line = split_table[2] # table column line
|
256
298
|
unit_line = split_table[3] # column unit line
|
257
|
-
length_line = [len(val) + 1 for val in split_table[4].split(
|
299
|
+
length_line = [len(val) + 1 for val in split_table[4].split("|")] # length line
|
258
300
|
|
259
301
|
# iterate over header values and units to build peak table regex
|
260
302
|
peak_re_string = []
|
261
303
|
for header, unit in zip(
|
262
|
-
|
263
|
-
chunk_string(unit_line, length_line)
|
304
|
+
chunk_string(column_line, length_line), chunk_string(unit_line, length_line)
|
264
305
|
):
|
265
|
-
if header ==
|
306
|
+
if header == "": # todo create a better catch for an undefined header
|
266
307
|
continue
|
267
308
|
try:
|
268
309
|
peak_re_string.append(
|
269
|
-
self._column_re_dictionary[header][
|
310
|
+
self._column_re_dictionary[header][
|
311
|
+
unit
|
312
|
+
] # append the appropriate regex
|
270
313
|
)
|
271
314
|
except KeyError: # catch for undefined regexes (need to be built)
|
272
|
-
raise KeyError(
|
273
|
-
|
315
|
+
raise KeyError(
|
316
|
+
f'The header/unit combination "{header}" "{unit}" is not defined in the peak regex '
|
317
|
+
f"dictionary. Let Lars know."
|
318
|
+
)
|
274
319
|
return re.compile(
|
275
|
-
|
276
|
-
|
320
|
+
"[ ]+".join(
|
321
|
+
peak_re_string
|
322
|
+
) # constructed string delimited by 1 or more spaces
|
323
|
+
+ "[\s]*" # and any remaining white space
|
277
324
|
)
|
pychemstation/control/README.md
CHANGED
@@ -1,28 +1,24 @@
|
|
1
|
-
#
|
1
|
+
# Examples of usecases
|
2
2
|
|
3
|
-
## Initialization
|
4
3
|
```python
|
5
4
|
from pychemstation.control import HPLCController
|
6
5
|
|
7
6
|
DEFAULT_METHOD_DIR = "C:\\ChemStation\\1\\Methods\\"
|
8
|
-
DATA_DIR = "C:\\Users\\Public\\Documents\\ChemStation\\3\\Data"
|
9
7
|
SEQUENCE_DIR = "C:\\USERS\\PUBLIC\\DOCUMENTS\\CHEMSTATION\\3\\Sequence"
|
10
8
|
DEFAULT_COMMAND_PATH = "C:\\Users\\User\\Desktop\\Lucy\\"
|
9
|
+
DATA_DIR_2 = "C:\\Users\\Public\\Documents\\ChemStation\\2\\Data"
|
10
|
+
DATA_DIR_3 = "C:\\Users\\Public\\Documents\\ChemStation\\3\\Data"
|
11
11
|
|
12
|
-
|
12
|
+
# Initialize HPLC Controller
|
13
|
+
hplc_controller = HPLCController(data_dirs=[DATA_DIR_2, DATA_DIR_3],
|
13
14
|
comm_dir=DEFAULT_COMMAND_PATH,
|
14
15
|
method_dir=DEFAULT_METHOD_DIR,
|
15
16
|
sequence_dir=SEQUENCE_DIR)
|
16
|
-
```
|
17
17
|
|
18
|
-
|
19
|
-
```python
|
18
|
+
# Switching a method
|
20
19
|
hplc_controller.switch_method("General-Poroshell")
|
21
|
-
```
|
22
|
-
|
23
|
-
## Editing a method
|
24
20
|
|
25
|
-
|
21
|
+
# Editing a method
|
26
22
|
from pychemstation.utils.method_types import *
|
27
23
|
|
28
24
|
new_method = MethodDetails(
|
@@ -45,47 +41,27 @@ new_method = MethodDetails(
|
|
45
41
|
stop_time=5,
|
46
42
|
post_time=2
|
47
43
|
)
|
48
|
-
|
49
44
|
hplc_controller.edit_method(new_method)
|
50
|
-
```
|
51
45
|
|
52
|
-
|
53
|
-
```python
|
46
|
+
# Run a method and get a report or data from last run method
|
54
47
|
hplc_controller.run_method(experiment_name="test_experiment")
|
55
48
|
chrom = hplc_controller.get_last_run_method_data()
|
56
49
|
channel_a_time = chrom.A.x
|
57
|
-
|
50
|
+
report = hplc_controller.get_last_run_method_report()
|
51
|
+
vial_location = report.vial_location
|
58
52
|
|
59
|
-
|
60
|
-
```python
|
53
|
+
# switch the currently loaded sequence
|
61
54
|
hplc_controller.switch_sequence(sequence_name="hplc_testing")
|
62
|
-
```
|
63
|
-
## Editing a Sequence Row
|
64
|
-
```python
|
65
|
-
from pychemstation.utils.sequence_types import *
|
66
|
-
from pychemstation.utils.tray_types import *
|
67
55
|
|
68
|
-
|
69
|
-
vial_location=FiftyFourVialPlate(plate=Plate.TWO, letter=Letter.A, num=Num.SEVEN).value(),
|
70
|
-
method="General-Poroshell",
|
71
|
-
num_inj=3,
|
72
|
-
inj_vol=4,
|
73
|
-
sample_name="Blank",
|
74
|
-
sample_type=SampleType.BLANK,
|
75
|
-
inj_source=InjectionSource.HIP_ALS
|
76
|
-
), 1)
|
77
|
-
```
|
78
|
-
|
79
|
-
## Editing entire Sequence Table
|
80
|
-
```python
|
56
|
+
# edit the sequence table
|
81
57
|
from pychemstation.utils.tray_types import *
|
82
58
|
from pychemstation.utils.sequence_types import *
|
83
59
|
|
84
60
|
seq_table = SequenceTable(
|
85
|
-
name=
|
61
|
+
name="hplc_testing",
|
86
62
|
rows=[
|
87
63
|
SequenceEntry(
|
88
|
-
vial_location=FiftyFourVialPlate
|
64
|
+
vial_location=FiftyFourVialPlate.from_str("P1-A1"),
|
89
65
|
method="General-Poroshell",
|
90
66
|
num_inj=3,
|
91
67
|
inj_vol=4,
|
@@ -94,7 +70,7 @@ seq_table = SequenceTable(
|
|
94
70
|
inj_source=InjectionSource.MANUAL
|
95
71
|
),
|
96
72
|
SequenceEntry(
|
97
|
-
vial_location=TenVialColumn.ONE
|
73
|
+
vial_location=TenVialColumn.ONE,
|
98
74
|
method="General-Poroshell",
|
99
75
|
num_inj=1,
|
100
76
|
inj_vol=1,
|
@@ -103,7 +79,7 @@ seq_table = SequenceTable(
|
|
103
79
|
inj_source=InjectionSource.AS_METHOD
|
104
80
|
),
|
105
81
|
SequenceEntry(
|
106
|
-
vial_location=
|
82
|
+
vial_location=FiftyFourVialPlate.from_str("P2-B4"),
|
107
83
|
method="General-Poroshell",
|
108
84
|
num_inj=3,
|
109
85
|
inj_vol=4,
|
@@ -114,11 +90,11 @@ seq_table = SequenceTable(
|
|
114
90
|
]
|
115
91
|
)
|
116
92
|
hplc_controller.edit_sequence(seq_table)
|
117
|
-
```
|
118
93
|
|
119
|
-
|
120
|
-
|
121
|
-
hplc_controller.
|
122
|
-
|
123
|
-
|
94
|
+
# Run a sequence and get data or report from last run sequence
|
95
|
+
hplc_controller.run_sequence()
|
96
|
+
chroms = hplc_controller.get_last_run_sequence_data(read_uv=True)
|
97
|
+
row_1_channel_A_abs = chroms[0][210].y
|
98
|
+
report = hplc_controller.get_last_run_sequence_reports()
|
99
|
+
vial_location_row_1 = report[0].vial_location
|
124
100
|
```
|
@@ -9,13 +9,21 @@ been processed.
|
|
9
9
|
|
10
10
|
Authors: Alexander Hammer, Hessam Mehr, Lucy Hao
|
11
11
|
"""
|
12
|
+
|
12
13
|
import os
|
13
14
|
import time
|
14
|
-
from typing import Optional
|
15
|
+
from typing import Optional, Union
|
15
16
|
|
16
|
-
from result import
|
17
|
+
from result import Err, Ok, Result
|
17
18
|
|
18
|
-
from ...utils.macro import
|
19
|
+
from ...utils.macro import (
|
20
|
+
str_to_status,
|
21
|
+
HPLCAvailStatus,
|
22
|
+
HPLCErrorStatus,
|
23
|
+
Command,
|
24
|
+
Status,
|
25
|
+
Response,
|
26
|
+
)
|
19
27
|
|
20
28
|
|
21
29
|
class CommunicationController:
|
@@ -27,11 +35,11 @@ class CommunicationController:
|
|
27
35
|
MAX_CMD_NO = 255
|
28
36
|
|
29
37
|
def __init__(
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
38
|
+
self,
|
39
|
+
comm_dir: str,
|
40
|
+
cmd_file: str = "cmd",
|
41
|
+
reply_file: str = "reply",
|
42
|
+
debug: bool = False,
|
35
43
|
):
|
36
44
|
"""
|
37
45
|
:param comm_dir:
|
@@ -55,7 +63,7 @@ class CommunicationController:
|
|
55
63
|
self.reset_cmd_counter()
|
56
64
|
|
57
65
|
# Initialize row counter for table operations
|
58
|
-
self.send(
|
66
|
+
self.send("Local Rows")
|
59
67
|
|
60
68
|
def get_num_val(self, cmd: str) -> Union[int, float]:
|
61
69
|
tries = 5
|
@@ -138,6 +146,7 @@ class CommunicationController:
|
|
138
146
|
:return: Potential ChemStation response
|
139
147
|
"""
|
140
148
|
err: Optional[Union[OSError, IndexError]] = None
|
149
|
+
err_msg = ""
|
141
150
|
for _ in range(num_attempts):
|
142
151
|
time.sleep(1)
|
143
152
|
|
@@ -150,7 +159,11 @@ class CommunicationController:
|
|
150
159
|
|
151
160
|
try:
|
152
161
|
first_line = response.splitlines()[0]
|
153
|
-
|
162
|
+
try:
|
163
|
+
response_no = int(first_line.split()[0])
|
164
|
+
except ValueError as e:
|
165
|
+
err = e
|
166
|
+
err_msg = f"Caused by {first_line}"
|
154
167
|
except IndexError as e:
|
155
168
|
err = e
|
156
169
|
continue
|
@@ -161,7 +174,7 @@ class CommunicationController:
|
|
161
174
|
else:
|
162
175
|
continue
|
163
176
|
else:
|
164
|
-
return Err(f"Failed to receive reply to command #{cmd_no} due to {err}.")
|
177
|
+
return Err(f"Failed to receive reply to command #{cmd_no} due to {err} caused by {err_msg}.")
|
165
178
|
|
166
179
|
def sleepy_send(self, cmd: Union[Command, str]):
|
167
180
|
self.send("Sleep 0.1")
|
@@ -187,20 +200,28 @@ class CommunicationController:
|
|
187
200
|
def receive(self) -> Result[Response, str]:
|
188
201
|
"""Returns messages received in reply file.
|
189
202
|
|
190
|
-
:return: ChemStation response
|
203
|
+
:return: ChemStation response
|
191
204
|
"""
|
192
205
|
num_response_prefix = "Numerical Responses:"
|
193
206
|
str_response_prefix = "String Responses:"
|
194
207
|
possible_response = self._receive(self.cmd_no)
|
195
208
|
if possible_response.is_ok():
|
196
|
-
lines = possible_response.
|
209
|
+
lines = possible_response.ok_value.splitlines()
|
197
210
|
for line in lines:
|
198
211
|
if str_response_prefix in line and num_response_prefix in line:
|
199
|
-
string_responses_dirty, _, numerical_responses = line.partition(
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
212
|
+
string_responses_dirty, _, numerical_responses = line.partition(
|
213
|
+
num_response_prefix
|
214
|
+
)
|
215
|
+
_, _, string_responses = string_responses_dirty.partition(
|
216
|
+
str_response_prefix
|
217
|
+
)
|
218
|
+
return Ok(
|
219
|
+
Response(
|
220
|
+
string_response=string_responses.strip(),
|
221
|
+
num_response=float(numerical_responses.strip()),
|
222
|
+
)
|
223
|
+
)
|
224
|
+
return Err("Could not retrieve HPLC response")
|
204
225
|
else:
|
205
226
|
return Err(f"Could not establish response to HPLC: {possible_response}")
|
206
227
|
|