pychemstation 0.7.0.dev2__py3-none-any.whl → 0.8.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/analysis/base_spectrum.py +3 -6
- pychemstation/analysis/process_report.py +248 -225
- pychemstation/analysis/utils.py +3 -1
- pychemstation/control/README.md +124 -0
- pychemstation/control/controllers/README.md +1 -0
- pychemstation/control/controllers/__init__.py +0 -2
- pychemstation/control/controllers/comm.py +27 -20
- pychemstation/control/controllers/devices/device.py +17 -4
- pychemstation/control/controllers/tables/method.py +57 -39
- pychemstation/control/controllers/tables/sequence.py +98 -28
- pychemstation/control/controllers/tables/table.py +121 -126
- pychemstation/control/hplc.py +82 -37
- pychemstation/generated/dad_method.py +3 -3
- pychemstation/generated/pump_method.py +7 -7
- pychemstation/out.txt +145 -0
- pychemstation/tests.ipynb +310 -0
- pychemstation/utils/chromatogram.py +5 -1
- pychemstation/utils/injector_types.py +2 -2
- pychemstation/utils/macro.py +1 -1
- pychemstation/utils/table_types.py +3 -0
- pychemstation/utils/tray_types.py +59 -39
- {pychemstation-0.7.0.dev2.dist-info → pychemstation-0.8.0.dist-info}/METADATA +25 -21
- pychemstation-0.8.0.dist-info/RECORD +39 -0
- {pychemstation-0.7.0.dev2.dist-info → pychemstation-0.8.0.dist-info}/WHEEL +1 -2
- pychemstation/control/comm.py +0 -206
- pychemstation/control/controllers/devices/column.py +0 -12
- pychemstation/control/controllers/devices/dad.py +0 -0
- pychemstation/control/controllers/devices/pump.py +0 -43
- pychemstation/control/controllers/method.py +0 -338
- pychemstation/control/controllers/sequence.py +0 -190
- pychemstation/control/controllers/table_controller.py +0 -266
- pychemstation/control/table/__init__.py +0 -3
- pychemstation/control/table/method.py +0 -274
- pychemstation/control/table/sequence.py +0 -210
- pychemstation/control/table/table_controller.py +0 -201
- pychemstation-0.7.0.dev2.dist-info/RECORD +0 -58
- pychemstation-0.7.0.dev2.dist-info/top_level.txt +0 -2
- tests/__init__.py +0 -0
- tests/constants.py +0 -88
- tests/test_comb.py +0 -136
- tests/test_comm.py +0 -65
- tests/test_inj.py +0 -39
- tests/test_method.py +0 -99
- tests/test_nightly.py +0 -80
- tests/test_proc_rep.py +0 -52
- tests/test_runs_stable.py +0 -125
- tests/test_sequence.py +0 -125
- tests/test_stable.py +0 -276
- {pychemstation-0.7.0.dev2.dist-info → pychemstation-0.8.0.dist-info/licenses}/LICENSE +0 -0
@@ -1,16 +1,13 @@
|
|
1
|
-
import pickle
|
2
|
-
import os
|
3
1
|
import logging
|
4
|
-
|
2
|
+
import os
|
3
|
+
import pickle
|
5
4
|
from abc import ABC, abstractmethod
|
6
5
|
|
7
|
-
import numpy as np
|
8
6
|
import matplotlib.pyplot as plt
|
9
|
-
|
7
|
+
import numpy as np
|
10
8
|
from scipy import (
|
11
9
|
sparse,
|
12
10
|
signal,
|
13
|
-
interpolate,
|
14
11
|
integrate,
|
15
12
|
)
|
16
13
|
|
@@ -1,254 +1,277 @@
|
|
1
|
+
import abc
|
1
2
|
import os
|
2
3
|
import re
|
4
|
+
from abc import abstractmethod
|
3
5
|
from dataclasses import dataclass
|
4
|
-
from
|
6
|
+
from enum import Enum
|
7
|
+
from typing import List, AnyStr, Dict, Optional, Pattern
|
5
8
|
|
6
9
|
import pandas as pd
|
7
10
|
from aghplctools.ingestion.text import _no_peaks_re, _area_report_re, _header_block_re, _signal_info_re, \
|
8
11
|
_signal_table_re, chunk_string
|
9
12
|
from result import Result, Err, Ok
|
10
13
|
|
14
|
+
from pychemstation.utils.chromatogram import AgilentHPLCChromatogram
|
11
15
|
from pychemstation.utils.tray_types import Tray, FiftyFourVialPlate
|
12
16
|
|
13
17
|
|
14
18
|
@dataclass
|
15
19
|
class AgilentPeak:
|
16
|
-
peak_number: int
|
17
20
|
retention_time: float
|
18
|
-
peak_type: str
|
19
21
|
width: float
|
20
22
|
area: float
|
21
23
|
height: float
|
22
|
-
|
24
|
+
peak_number: Optional[int]
|
25
|
+
peak_type: Optional[str]
|
26
|
+
height_percent: Optional[float]
|
27
|
+
|
28
|
+
|
29
|
+
@dataclass
|
30
|
+
class Signals:
|
31
|
+
wavelength: int
|
32
|
+
peaks: List[AgilentPeak]
|
33
|
+
data: Optional[AgilentHPLCChromatogram]
|
23
34
|
|
24
35
|
|
25
36
|
@dataclass
|
26
37
|
class AgilentReport:
|
27
|
-
vial_location: Tray
|
28
|
-
signals:
|
29
|
-
solvents: Dict[AnyStr, AnyStr]
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
38
|
+
vial_location: Optional[Tray]
|
39
|
+
signals: List[Signals]
|
40
|
+
solvents: Optional[Dict[AnyStr, AnyStr]]
|
41
|
+
|
42
|
+
|
43
|
+
class ReportType(Enum):
|
44
|
+
TXT = 0
|
45
|
+
CSV = 1
|
46
|
+
|
47
|
+
|
48
|
+
class ReportProcessor(abc.ABC):
|
49
|
+
def __init__(self, path: str):
|
50
|
+
self.path = path
|
51
|
+
|
52
|
+
@abstractmethod
|
53
|
+
def process_report(self) -> Result[AgilentReport, AnyStr]:
|
54
|
+
pass
|
55
|
+
|
56
|
+
|
57
|
+
class CSVProcessor(ReportProcessor):
|
58
|
+
def __init__(self, path: str):
|
59
|
+
"""
|
60
|
+
Class to process reports in CSV form.
|
61
|
+
|
62
|
+
:param path: the parent folder that contains the CSV report(s) to parse.
|
63
|
+
"""
|
64
|
+
super().__init__(path)
|
65
|
+
|
66
|
+
def process_report(self) -> Result[AgilentReport, AnyStr]:
|
67
|
+
"""
|
68
|
+
Method to parse details from CSV report.
|
69
|
+
|
70
|
+
:returns: subset of complete report details, specifically the sample location, solvents in pumps,
|
71
|
+
and list of peaks at each wavelength channel.
|
72
|
+
"""
|
73
|
+
labels = os.path.join(self.path, f'REPORT00.CSV')
|
74
|
+
if os.path.exists(labels):
|
75
|
+
df_labels: Dict[int, Dict[int: AnyStr]] = pd.read_csv(labels, encoding="utf-16", header=None).to_dict()
|
76
|
+
vial_location = []
|
77
|
+
signals = {}
|
78
|
+
solvents = {}
|
79
|
+
for pos, val in df_labels[0].items():
|
80
|
+
if val == "Location":
|
81
|
+
vial_location = df_labels[1][pos]
|
82
|
+
elif "Solvent" in val:
|
83
|
+
if val not in solvents.keys():
|
84
|
+
solvents[val] = df_labels[2][pos]
|
85
|
+
elif val == "Number of Signals":
|
86
|
+
num_signals = int(df_labels[1][pos])
|
87
|
+
for s in range(1, num_signals + 1):
|
88
|
+
df = pd.read_csv(os.path.join(self.path, f'REPORT0{s}.CSV'),
|
89
|
+
encoding="utf-16", header=None)
|
90
|
+
peaks = df.apply(lambda row: AgilentPeak(*row), axis=1)
|
91
|
+
wavelength = df_labels[1][pos + s].partition(",4 Ref=off")[0][-3:]
|
92
|
+
signals[wavelength] = peaks
|
93
|
+
break
|
61
94
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
return None
|
68
|
-
# todo verify that these indicies are always true
|
69
|
-
column_line = split_table[2] # table column line
|
70
|
-
unit_line = split_table[3] # column unit line
|
71
|
-
length_line = [len(val) + 1 for val in split_table[4].split('|')] # length line
|
72
|
-
|
73
|
-
# iterate over header values and units to build peak table regex
|
74
|
-
peak_re_string = []
|
75
|
-
for header, unit in zip(
|
76
|
-
chunk_string(column_line, length_line),
|
77
|
-
chunk_string(unit_line, length_line)
|
78
|
-
):
|
79
|
-
if header == '': # todo create a better catch for an undefined header
|
80
|
-
continue
|
81
|
-
try:
|
82
|
-
peak_re_string.append(
|
83
|
-
_column_re_dictionary[header][unit] # append the appropriate regex
|
84
|
-
)
|
85
|
-
except KeyError: # catch for undefined regexes (need to be built)
|
86
|
-
raise KeyError(f'The header/unit combination "{header}" "{unit}" is not defined in the peak regex '
|
87
|
-
f'dictionary. Let Lars know.')
|
88
|
-
return re.compile(
|
89
|
-
'[ ]+'.join(peak_re_string) # constructed string delimited by 1 or more spaces
|
90
|
-
+ '[\s]*' # and any remaining white space
|
91
|
-
)
|
95
|
+
return Ok(AgilentReport(
|
96
|
+
signals=[Signals(wavelength=w, peaks=s, data=None) for w, s in signals.items()],
|
97
|
+
vial_location=FiftyFourVialPlate.from_int(int(vial_location)),
|
98
|
+
solvents=solvents
|
99
|
+
))
|
92
100
|
|
101
|
+
return Err("No report found")
|
93
102
|
|
94
|
-
# todo should be able to use the parse_area_report method of aghplctools v4.8.8
|
95
103
|
|
96
|
-
|
104
|
+
class TXTProcessor(ReportProcessor):
|
97
105
|
"""
|
98
|
-
|
99
|
-
|
100
|
-
:param report_text: plain text version of the report.
|
101
|
-
:raises ValueError: if there are no peaks defined in the report text file
|
102
|
-
:return: dictionary of signals in the form
|
103
|
-
dict[wavelength][retention time (float)][Width/Area/Height/etc.]
|
106
|
+
Regex matches for column and unit combinations, courtesy of Veronica Lai.
|
104
107
|
"""
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
text
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
print(e)
|
161
|
-
return [], [], []
|
162
|
-
|
163
|
-
# filter wavelengths by the ones to keep
|
164
|
-
if target_wavelengths is not None:
|
165
|
-
signals = {key: signals[key] for key in target_wavelengths if key in signals}
|
166
|
-
|
167
|
-
wavelengths = []
|
168
|
-
retention_times = []
|
169
|
-
areas = []
|
170
|
-
|
171
|
-
for wavelength, wavelength_dict in signals.items():
|
172
|
-
for ret_time, ret_time_dict in wavelength_dict.items():
|
173
|
-
if min_retention_time <= ret_time <= max_retention_time:
|
174
|
-
wavelengths.append(wavelength)
|
175
|
-
retention_times.append(ret_time)
|
176
|
-
areas.append(ret_time_dict['Area'])
|
177
|
-
|
178
|
-
return wavelengths, retention_times, areas
|
179
|
-
|
180
|
-
|
181
|
-
def process_folder(folder_path, target_wavelengths=None, min_retention_time=0, max_retention_time=999):
|
182
|
-
# folder path is the path to the overall folder, and inside there should be subfolders for each LC sample
|
183
|
-
# each subfolder should have a Report.TXT file
|
184
|
-
# sample_names = []
|
185
|
-
wavelengths = []
|
186
|
-
retention_times = []
|
187
|
-
peak_areas = []
|
188
|
-
|
189
|
-
# Get a list of all items (files and directories) in the folder
|
190
|
-
items = [os.path.join(folder_path, item) for item in os.listdir(folder_path)]
|
191
|
-
|
192
|
-
# Filter only directories from the list
|
193
|
-
# folders = [item for item in items if os.path.isdir(item)]
|
194
|
-
|
195
|
-
# # Sort the folders by creation date
|
196
|
-
# sorted_folders = sorted(folders, key=lambda f: os.stat(f).st_ctime)
|
197
|
-
|
198
|
-
for filename in items:
|
199
|
-
if filename.endswith('Report.TXT'):
|
200
|
-
# file_path = os.path.join(subfolder, filename)
|
201
|
-
file_wavelengths, file_retention_times, file_peak_areas = process_export_report(filename,
|
202
|
-
target_wavelengths,
|
203
|
-
min_retention_time,
|
204
|
-
max_retention_time)
|
205
|
-
wavelengths.extend(file_wavelengths)
|
206
|
-
retention_times.extend(file_retention_times)
|
207
|
-
peak_areas.extend(file_peak_areas)
|
208
|
-
|
209
|
-
results_df = pd.DataFrame({'Wavelengths': wavelengths, 'Retention Times': retention_times, 'Areas': peak_areas})
|
210
|
-
|
211
|
-
# Save the results to a CSV file
|
212
|
-
# results_csv_path = os.path.join(folder_path, 'all_sample_data.csv') # Specify the desired file path
|
213
|
-
# results_df.to_csv(results_csv_path, index=False)
|
214
|
-
# print(f"Results saved to {results_csv_path}")
|
215
|
-
return results_df
|
216
|
-
|
217
|
-
|
218
|
-
def process_csv_report(folder_path: str) -> Result[AgilentReport, AnyStr]:
|
219
|
-
labels = os.path.join(folder_path, f'REPORT00.CSV')
|
220
|
-
if os.path.exists(labels):
|
221
|
-
df_labels: Dict[int, Dict[int: AnyStr]] = pd.read_csv(labels, encoding="utf-16", header=None).to_dict()
|
222
|
-
vial_location = []
|
223
|
-
signals = {}
|
224
|
-
solvents = {}
|
225
|
-
for pos, val in df_labels[0].items():
|
226
|
-
if val == "Location":
|
227
|
-
vial_location = df_labels[1][pos]
|
228
|
-
elif "Solvent" in val:
|
229
|
-
if val not in solvents.keys():
|
230
|
-
solvents[val] = df_labels[2][pos]
|
231
|
-
elif val == "Number of Signals":
|
232
|
-
num_signals = int(df_labels[1][pos])
|
233
|
-
for s in range(1, num_signals + 1):
|
234
|
-
peaks = process_peaks(os.path.join(folder_path, f'REPORT0{s}.CSV'))
|
235
|
-
if peaks.is_ok():
|
236
|
-
wavelength = df_labels[1][pos + s].partition(",4 Ref=off")[0][-3:]
|
237
|
-
signals[wavelength] = peaks.ok_value
|
238
|
-
break
|
239
|
-
|
240
|
-
return Ok(AgilentReport(
|
241
|
-
signals=signals,
|
242
|
-
vial_location=FiftyFourVialPlate.from_int(vial_location),
|
243
|
-
solvents=solvents
|
244
|
-
))
|
108
|
+
_column_re_dictionary = {
|
109
|
+
'Peak': { # peak index
|
110
|
+
'#': '[ ]+(?P<Peak>[\d]+)', # number
|
111
|
+
},
|
112
|
+
'RetTime': { # retention time
|
113
|
+
'[min]': '(?P<RetTime>[\d]+.[\d]+)', # minutes
|
114
|
+
},
|
115
|
+
'Type': { # peak type
|
116
|
+
'': '(?P<Type>[A-Z]{1,3}(?: [A-Z]{1,2})*)', # todo this is different from <4.8.8 aghplc tools
|
117
|
+
},
|
118
|
+
'Width': { # peak width
|
119
|
+
'[min]': '(?P<Width>[\d]+.[\d]+[e+-]*[\d]+)',
|
120
|
+
},
|
121
|
+
'Area': { # peak area
|
122
|
+
'[mAU*s]': '(?P<Area>[\d]+.[\d]+[e+-]*[\d]+)', # area units
|
123
|
+
'%': '(?P<percent>[\d]+.[\d]+[e+-]*[\d]+)', # percent
|
124
|
+
},
|
125
|
+
'Height': { # peak height
|
126
|
+
'[mAU]': '(?P<Height>[\d]+.[\d]+[e+-]*[\d]+)',
|
127
|
+
},
|
128
|
+
'Name': {
|
129
|
+
'': '(?P<Name>[^\s]+(?:\s[^\s]+)*)', # peak name
|
130
|
+
},
|
131
|
+
}
|
132
|
+
|
133
|
+
def __init__(self, path: str, min_ret_time: int = 0,
|
134
|
+
max_ret_time: int = 999,
|
135
|
+
target_wavelength_range: List[int] = range(200, 300)):
|
136
|
+
"""
|
137
|
+
Class to process reports in CSV form.
|
138
|
+
|
139
|
+
:param path: the parent folder that contains the CSV report(s) to parse.
|
140
|
+
:param min_ret_time: peaks after this value (min) will be returned
|
141
|
+
:param max_ret_time: peaks will only be returned up to this time (min)
|
142
|
+
:param target_wavelength_range: range of wavelengths to return
|
143
|
+
"""
|
144
|
+
self.target_wavelength_range = target_wavelength_range
|
145
|
+
self.min_ret_time = min_ret_time
|
146
|
+
self.max_ret_time = max_ret_time
|
147
|
+
super().__init__(path)
|
148
|
+
|
149
|
+
def process_report(self) -> Result[AgilentReport, AnyStr]:
|
150
|
+
"""
|
151
|
+
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
|
+
If you want more functionality, use `aghplctools`.
|
157
|
+
`from aghplctools.ingestion.text import pull_hplc_area_from_txt`
|
158
|
+
`signals = pull_hplc_area_from_txt(file_path)`
|
159
|
+
"""
|
160
|
+
|
161
|
+
with open(os.path.join(self.path, "REPORT.TXT"), 'r', encoding='utf-16') as openfile:
|
162
|
+
text = openfile.read()
|
245
163
|
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
164
|
+
try:
|
165
|
+
signals = self.parse_area_report(text)
|
166
|
+
except ValueError as e:
|
167
|
+
return Err("No peaks found: " + str(e))
|
168
|
+
|
169
|
+
signals = {key: signals[key] for key in self.target_wavelength_range if key in signals}
|
170
|
+
|
171
|
+
parsed_signals = []
|
172
|
+
for wavelength, wavelength_dict in signals.items():
|
173
|
+
current_wavelength_signals = Signals(wavelength=wavelength, peaks=[], data=None)
|
174
|
+
for ret_time, ret_time_dict in wavelength_dict.items():
|
175
|
+
if self.min_ret_time <= ret_time <= self.max_ret_time:
|
176
|
+
current_wavelength_signals.peaks.append(AgilentPeak(retention_time=ret_time,
|
177
|
+
area=ret_time_dict['Area'],
|
178
|
+
width=ret_time_dict['Width'],
|
179
|
+
height=ret_time_dict['Height'],
|
180
|
+
peak_number=None,
|
181
|
+
peak_type=ret_time_dict['Type'],
|
182
|
+
height_percent=None))
|
183
|
+
parsed_signals.append(current_wavelength_signals)
|
184
|
+
|
185
|
+
return Ok(AgilentReport(vial_location=None,
|
186
|
+
solvents=None,
|
187
|
+
signals=parsed_signals))
|
188
|
+
|
189
|
+
def parse_area_report(self, report_text: str) -> Dict:
|
190
|
+
"""
|
191
|
+
Interprets report text and parses the area report section, converting it to dictionary.
|
192
|
+
Courtesy of Veronica Lai.
|
193
|
+
|
194
|
+
:param report_text: plain text version of the report.
|
195
|
+
:raises ValueError: if there are no peaks defined in the report text file
|
196
|
+
:return: dictionary of signals in the form
|
197
|
+
dict[wavelength][retention time (float)][Width/Area/Height/etc.]
|
198
|
+
|
199
|
+
If you want more functionality, use `aghplctools`.
|
200
|
+
should be able to use the `parse_area_report` method of aghplctools v4.8.8
|
201
|
+
"""
|
202
|
+
if re.search(_no_peaks_re, report_text): # There are no peaks in Report.txt
|
203
|
+
raise ValueError(f'No peaks found in Report.txt')
|
204
|
+
blocks = _header_block_re.split(report_text)
|
205
|
+
signals = {} # output dictionary
|
206
|
+
for ind, block in enumerate(blocks):
|
207
|
+
# area report block
|
208
|
+
if _area_report_re.match(block): # match area report block
|
209
|
+
# break into signal blocks
|
210
|
+
signal_blocks = _signal_table_re.split(blocks[ind + 1])
|
211
|
+
# iterate over signal blocks
|
212
|
+
for table in signal_blocks:
|
213
|
+
si = _signal_info_re.match(table)
|
214
|
+
if si is not None:
|
215
|
+
# some error state (e.g. 'not found')
|
216
|
+
if si.group('error') != '':
|
217
|
+
continue
|
218
|
+
wavelength = float(si.group('wavelength'))
|
219
|
+
if wavelength in signals:
|
220
|
+
# placeholder error raise just in case (this probably won't happen)
|
221
|
+
raise KeyError(
|
222
|
+
f'The wavelength {float(si.group("wavelength"))} is already in the signals dictionary')
|
223
|
+
signals[wavelength] = {}
|
224
|
+
# build peak regex
|
225
|
+
peak_re = self.build_peak_regex(table)
|
226
|
+
if peak_re is None: # if there are no columns (empty table), continue
|
227
|
+
continue
|
228
|
+
for line in table.split('\n'):
|
229
|
+
peak = peak_re.match(line)
|
230
|
+
if peak is not None:
|
231
|
+
signals[wavelength][float(peak.group('RetTime'))] = {}
|
232
|
+
current = signals[wavelength][float(peak.group('RetTime'))]
|
233
|
+
for key in self._column_re_dictionary:
|
234
|
+
if key in peak.re.groupindex:
|
235
|
+
try: # try float conversion, otherwise continue
|
236
|
+
value = float(peak.group(key))
|
237
|
+
except ValueError:
|
238
|
+
value = peak.group(key)
|
239
|
+
current[key] = value
|
240
|
+
else: # ensures defined
|
241
|
+
current[key] = None
|
242
|
+
return signals
|
243
|
+
|
244
|
+
def build_peak_regex(self, signal_table: str) -> Pattern[AnyStr]:
|
245
|
+
"""
|
246
|
+
Builds a peak regex from a signal table. Courtesy of Veronica Lai.
|
247
|
+
|
248
|
+
:param signal_table: block of lines associated with an area table
|
249
|
+
:return: peak line regex object (<=3.6 _sre.SRE_PATTERN, >=3.7 re.Pattern)
|
250
|
+
"""
|
251
|
+
split_table = signal_table.split('\n')
|
252
|
+
if len(split_table) <= 4: # catch peak table with no values
|
253
|
+
return None
|
254
|
+
# todo verify that these indicies are always true
|
255
|
+
column_line = split_table[2] # table column line
|
256
|
+
unit_line = split_table[3] # column unit line
|
257
|
+
length_line = [len(val) + 1 for val in split_table[4].split('|')] # length line
|
258
|
+
|
259
|
+
# iterate over header values and units to build peak table regex
|
260
|
+
peak_re_string = []
|
261
|
+
for header, unit in zip(
|
262
|
+
chunk_string(column_line, length_line),
|
263
|
+
chunk_string(unit_line, length_line)
|
264
|
+
):
|
265
|
+
if header == '': # todo create a better catch for an undefined header
|
266
|
+
continue
|
267
|
+
try:
|
268
|
+
peak_re_string.append(
|
269
|
+
self._column_re_dictionary[header][unit] # append the appropriate regex
|
270
|
+
)
|
271
|
+
except KeyError: # catch for undefined regexes (need to be built)
|
272
|
+
raise KeyError(f'The header/unit combination "{header}" "{unit}" is not defined in the peak regex '
|
273
|
+
f'dictionary. Let Lars know.')
|
274
|
+
return re.compile(
|
275
|
+
'[ ]+'.join(peak_re_string) # constructed string delimited by 1 or more spaces
|
276
|
+
+ '[\s]*' # and any remaining white space
|
277
|
+
)
|
pychemstation/analysis/utils.py
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
+
from typing import Tuple
|
2
|
+
|
1
3
|
import numpy as np
|
2
4
|
|
3
5
|
|
4
|
-
def find_nearest_value_index(array, value) ->
|
6
|
+
def find_nearest_value_index(array, value) -> Tuple[float, int]:
|
5
7
|
"""Returns closest value and its index in a given array.
|
6
8
|
|
7
9
|
:param array: An array to search in.
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# Examples of usecases
|
2
|
+
|
3
|
+
## Initialization
|
4
|
+
```python
|
5
|
+
from pychemstation.control import HPLCController
|
6
|
+
|
7
|
+
DEFAULT_METHOD_DIR = "C:\\ChemStation\\1\\Methods\\"
|
8
|
+
DATA_DIR = "C:\\Users\\Public\\Documents\\ChemStation\\3\\Data"
|
9
|
+
SEQUENCE_DIR = "C:\\USERS\\PUBLIC\\DOCUMENTS\\CHEMSTATION\\3\\Sequence"
|
10
|
+
DEFAULT_COMMAND_PATH = "C:\\Users\\User\\Desktop\\Lucy\\"
|
11
|
+
|
12
|
+
hplc_controller = HPLCController(data_dir=DATA_DIR,
|
13
|
+
comm_dir=DEFAULT_COMMAND_PATH,
|
14
|
+
method_dir=DEFAULT_METHOD_DIR,
|
15
|
+
sequence_dir=SEQUENCE_DIR)
|
16
|
+
```
|
17
|
+
|
18
|
+
## Switching a method
|
19
|
+
```python
|
20
|
+
hplc_controller.switch_method("General-Poroshell")
|
21
|
+
```
|
22
|
+
|
23
|
+
## Editing a method
|
24
|
+
|
25
|
+
```python
|
26
|
+
from pychemstation.utils.method_types import *
|
27
|
+
|
28
|
+
new_method = MethodDetails(
|
29
|
+
name="My_Method",
|
30
|
+
params=HPLCMethodParams(
|
31
|
+
organic_modifier=7,
|
32
|
+
flow=0.44),
|
33
|
+
timetable=[
|
34
|
+
TimeTableEntry(
|
35
|
+
start_time=0.10,
|
36
|
+
organic_modifer=7,
|
37
|
+
flow=0.34
|
38
|
+
),
|
39
|
+
TimeTableEntry(
|
40
|
+
start_time=1,
|
41
|
+
organic_modifer=99,
|
42
|
+
flow=0.55
|
43
|
+
)
|
44
|
+
],
|
45
|
+
stop_time=5,
|
46
|
+
post_time=2
|
47
|
+
)
|
48
|
+
|
49
|
+
hplc_controller.edit_method(new_method)
|
50
|
+
```
|
51
|
+
|
52
|
+
## Running a method and get data from last run method
|
53
|
+
```python
|
54
|
+
hplc_controller.run_method(experiment_name="test_experiment")
|
55
|
+
chrom = hplc_controller.get_last_run_method_data()
|
56
|
+
channel_a_time = chrom.A.x
|
57
|
+
```
|
58
|
+
|
59
|
+
## Switching a sequence
|
60
|
+
```python
|
61
|
+
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
|
+
|
68
|
+
hplc_controller.edit_sequence_row(SequenceEntry(
|
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
|
81
|
+
from pychemstation.utils.tray_types import *
|
82
|
+
from pychemstation.utils.sequence_types import *
|
83
|
+
|
84
|
+
seq_table = SequenceTable(
|
85
|
+
name=DEFAULT_SEQUENCE,
|
86
|
+
rows=[
|
87
|
+
SequenceEntry(
|
88
|
+
vial_location=FiftyFourVialPlate(plate=Plate.TWO, letter=Letter.A, num=Num.SEVEN).value(),
|
89
|
+
method="General-Poroshell",
|
90
|
+
num_inj=3,
|
91
|
+
inj_vol=4,
|
92
|
+
sample_name="Control",
|
93
|
+
sample_type=SampleType.CONTROL,
|
94
|
+
inj_source=InjectionSource.MANUAL
|
95
|
+
),
|
96
|
+
SequenceEntry(
|
97
|
+
vial_location=TenVialColumn.ONE.value,
|
98
|
+
method="General-Poroshell",
|
99
|
+
num_inj=1,
|
100
|
+
inj_vol=1,
|
101
|
+
sample_name="Sample",
|
102
|
+
sample_type=SampleType.SAMPLE,
|
103
|
+
inj_source=InjectionSource.AS_METHOD
|
104
|
+
),
|
105
|
+
SequenceEntry(
|
106
|
+
vial_location=10,
|
107
|
+
method="General-Poroshell",
|
108
|
+
num_inj=3,
|
109
|
+
inj_vol=4,
|
110
|
+
sample_name="Blank",
|
111
|
+
sample_type=SampleType.BLANK,
|
112
|
+
inj_source=InjectionSource.HIP_ALS
|
113
|
+
),
|
114
|
+
]
|
115
|
+
)
|
116
|
+
hplc_controller.edit_sequence(seq_table)
|
117
|
+
```
|
118
|
+
|
119
|
+
## Running a sequence and get data from last run sequence
|
120
|
+
```python
|
121
|
+
hplc_controller.run_sequence(seq_table)
|
122
|
+
chroms = hplc_controller.get_last_run_sequence_data()
|
123
|
+
channel_A_time = chroms[0].A.x
|
124
|
+
```
|
@@ -0,0 +1 @@
|
|
1
|
+
# Chemstation Devices and Tables
|