pychemstation 0.10.3__py3-none-any.whl → 0.10.5__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 +1 -6
- pychemstation/analysis/base_spectrum.py +7 -7
- pychemstation/{utils → analysis}/chromatogram.py +24 -4
- pychemstation/analysis/process_report.py +189 -90
- pychemstation/control/__init__.py +5 -2
- pychemstation/control/controllers/__init__.py +2 -7
- pychemstation/control/controllers/comm.py +56 -32
- pychemstation/control/controllers/devices/device.py +59 -24
- pychemstation/control/controllers/devices/injector.py +33 -10
- pychemstation/control/controllers/tables/__init__.py +4 -0
- pychemstation/control/controllers/tables/method.py +241 -151
- pychemstation/control/controllers/tables/sequence.py +226 -107
- pychemstation/control/controllers/tables/table.py +216 -132
- pychemstation/control/hplc.py +89 -75
- pychemstation/generated/__init__.py +0 -2
- pychemstation/generated/pump_method.py +15 -19
- pychemstation/utils/injector_types.py +1 -1
- pychemstation/utils/macro.py +11 -10
- pychemstation/utils/method_types.py +2 -1
- pychemstation/utils/parsing.py +0 -11
- pychemstation/utils/sequence_types.py +2 -3
- pychemstation/utils/spec_utils.py +2 -3
- pychemstation/utils/table_types.py +10 -9
- pychemstation/utils/tray_types.py +45 -36
- {pychemstation-0.10.3.dist-info → pychemstation-0.10.5.dist-info}/METADATA +5 -4
- pychemstation-0.10.5.dist-info/RECORD +36 -0
- pychemstation/control/controllers/tables/ms.py +0 -21
- pychemstation-0.10.3.dist-info/RECORD +0 -37
- {pychemstation-0.10.3.dist-info → pychemstation-0.10.5.dist-info}/WHEEL +0 -0
- {pychemstation-0.10.3.dist-info → pychemstation-0.10.5.dist-info}/licenses/LICENSE +0 -0
pychemstation/__init__.py
CHANGED
@@ -197,10 +197,10 @@ class AbstractSpectrum(ABC):
|
|
197
197
|
return (self.x.copy()[full_mask], self.y.copy()[full_mask])
|
198
198
|
|
199
199
|
def show_spectrum(
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
200
|
+
self,
|
201
|
+
filename=None,
|
202
|
+
title=None,
|
203
|
+
label=None,
|
204
204
|
):
|
205
205
|
"""Plots the spectral data using matplotlib.pyplot module.
|
206
206
|
|
@@ -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:
|
@@ -385,12 +385,12 @@ class AbstractSpectrum(ABC):
|
|
385
385
|
|
386
386
|
if rule == "trapz":
|
387
387
|
return integrate.trapz(
|
388
|
-
self.y[left_idx: right_idx + 1], self.x[left_idx: right_idx + 1]
|
388
|
+
self.y[left_idx : right_idx + 1], self.x[left_idx : right_idx + 1]
|
389
389
|
)
|
390
390
|
|
391
391
|
elif rule == "simps":
|
392
392
|
return integrate.simps(
|
393
|
-
self.y[left_idx: right_idx + 1], self.x[left_idx: right_idx + 1]
|
393
|
+
self.y[left_idx : right_idx + 1], self.x[left_idx : right_idx + 1]
|
394
394
|
)
|
395
395
|
|
396
396
|
else:
|
@@ -3,11 +3,13 @@
|
|
3
3
|
import os
|
4
4
|
import time
|
5
5
|
from dataclasses import dataclass
|
6
|
+
from typing import Dict
|
6
7
|
|
7
8
|
import numpy as np
|
8
9
|
|
9
|
-
|
10
|
-
from .parsing import CHFile
|
10
|
+
|
11
|
+
from ..utils.parsing import CHFile
|
12
|
+
from ..analysis.base_spectrum import AbstractSpectrum
|
11
13
|
|
12
14
|
ACQUISITION_PARAMETERS = "acq.txt"
|
13
15
|
|
@@ -36,12 +38,11 @@ class AgilentHPLCChromatogram(AbstractSpectrum):
|
|
36
38
|
}
|
37
39
|
|
38
40
|
def __init__(self, path=None, autosaving=False):
|
39
|
-
|
40
41
|
if path is not None:
|
41
42
|
os.makedirs(path, exist_ok=True)
|
42
43
|
self.path = path
|
43
44
|
else:
|
44
|
-
self.path = os.path.join("
|
45
|
+
self.path = os.path.join("../utils", "hplc_data")
|
45
46
|
os.makedirs(self.path, exist_ok=True)
|
46
47
|
|
47
48
|
super().__init__(path=path, autosaving=autosaving)
|
@@ -114,3 +115,22 @@ class AgilentChannelChromatogramData:
|
|
114
115
|
F: AgilentHPLCChromatogram
|
115
116
|
G: AgilentHPLCChromatogram
|
116
117
|
H: AgilentHPLCChromatogram
|
118
|
+
|
119
|
+
@classmethod
|
120
|
+
def from_dict(cls, chroms: Dict[str, AgilentHPLCChromatogram]):
|
121
|
+
keys = chroms.keys()
|
122
|
+
class_keys = vars(AgilentChannelChromatogramData)["__annotations__"].keys()
|
123
|
+
if set(class_keys) == set(keys):
|
124
|
+
return AgilentChannelChromatogramData(
|
125
|
+
A=chroms["A"],
|
126
|
+
B=chroms["B"],
|
127
|
+
C=chroms["C"],
|
128
|
+
D=chroms["D"],
|
129
|
+
E=chroms["E"],
|
130
|
+
F=chroms["F"],
|
131
|
+
G=chroms["G"],
|
132
|
+
H=chroms["H"],
|
133
|
+
)
|
134
|
+
else:
|
135
|
+
err = f"{keys} don't match {class_keys}"
|
136
|
+
raise KeyError(err)
|
@@ -1,10 +1,12 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import abc
|
2
4
|
import os
|
3
5
|
import re
|
4
6
|
from abc import abstractmethod
|
5
7
|
from dataclasses import dataclass
|
6
8
|
from enum import Enum
|
7
|
-
from typing import AnyStr, Dict, List, Optional, Pattern
|
9
|
+
from typing import AnyStr, Dict, List, Optional, Pattern, Union
|
8
10
|
|
9
11
|
import pandas as pd
|
10
12
|
from aghplctools.ingestion.text import (
|
@@ -15,10 +17,11 @@ from aghplctools.ingestion.text import (
|
|
15
17
|
_signal_table_re,
|
16
18
|
chunk_string,
|
17
19
|
)
|
20
|
+
from pandas.errors import EmptyDataError
|
18
21
|
from result import Err, Ok, Result
|
19
22
|
|
20
|
-
from
|
21
|
-
from
|
23
|
+
from ..analysis.chromatogram import AgilentHPLCChromatogram
|
24
|
+
from ..utils.tray_types import FiftyFourVialPlate, Tray
|
22
25
|
|
23
26
|
|
24
27
|
@dataclass
|
@@ -43,7 +46,7 @@ class Signals:
|
|
43
46
|
class AgilentReport:
|
44
47
|
vial_location: Optional[Tray]
|
45
48
|
signals: List[Signals]
|
46
|
-
solvents: Optional[Dict[
|
49
|
+
solvents: Optional[Dict[str, str]]
|
47
50
|
|
48
51
|
|
49
52
|
class ReportType(Enum):
|
@@ -69,6 +72,37 @@ class CSVProcessor(ReportProcessor):
|
|
69
72
|
"""
|
70
73
|
super().__init__(path)
|
71
74
|
|
75
|
+
def find_csv_prefix(self) -> str:
|
76
|
+
files = [
|
77
|
+
f
|
78
|
+
for f in os.listdir(self.path)
|
79
|
+
if os.path.isfile(os.path.join(self.path, f))
|
80
|
+
]
|
81
|
+
for file in files:
|
82
|
+
if "00" in file:
|
83
|
+
name, _, file_extension = file.partition(".")
|
84
|
+
if "00" in name and file_extension.lower() == "csv":
|
85
|
+
prefix, _, _ = name.partition("00")
|
86
|
+
return prefix
|
87
|
+
raise FileNotFoundError("Couldn't find the prefix for CSV")
|
88
|
+
|
89
|
+
def report_contains(self, labels: List[str], want: List[str]):
|
90
|
+
for label in labels:
|
91
|
+
if label in want:
|
92
|
+
want.remove(label)
|
93
|
+
|
94
|
+
all_labels_seen = False
|
95
|
+
if len(want) != 0:
|
96
|
+
for want_label in want:
|
97
|
+
label_seen = False
|
98
|
+
for label in labels:
|
99
|
+
if want_label in label or want_label == label:
|
100
|
+
label_seen = True
|
101
|
+
all_labels_seen = label_seen
|
102
|
+
else:
|
103
|
+
return True
|
104
|
+
return all_labels_seen
|
105
|
+
|
72
106
|
def process_report(self) -> Result[AgilentReport, AnyStr]:
|
73
107
|
"""
|
74
108
|
Method to parse details from CSV report.
|
@@ -76,13 +110,30 @@ class CSVProcessor(ReportProcessor):
|
|
76
110
|
:return: subset of complete report details, specifically the sample location, solvents in pumps,
|
77
111
|
and list of peaks at each wavelength channel.
|
78
112
|
"""
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
113
|
+
prefix = self.find_csv_prefix()
|
114
|
+
labels = os.path.join(self.path, f"{prefix}00.CSV")
|
115
|
+
if not os.path.exists(labels):
|
116
|
+
raise ValueError(
|
117
|
+
"CSV reports do not exist, make sure to turn on the post run CSV report option!"
|
118
|
+
)
|
119
|
+
elif os.path.exists(labels):
|
120
|
+
LOCATION = "Location"
|
121
|
+
NUM_SIGNALS = "Number of Signals"
|
122
|
+
SOLVENT = "Solvent"
|
123
|
+
df_labels: Dict[int, Dict[int, str]] = pd.read_csv(
|
124
|
+
labels, encoding="utf-16", header=None
|
125
|
+
).to_dict()
|
126
|
+
vial_location: str = ""
|
127
|
+
signals: Dict[int, list[AgilentPeak]] = {}
|
128
|
+
solvents: Dict[str, str] = {}
|
129
|
+
report_labels: Dict[int, str] = df_labels[0]
|
130
|
+
|
131
|
+
if not self.report_contains(
|
132
|
+
list(report_labels.values()), [LOCATION, NUM_SIGNALS, SOLVENT]
|
133
|
+
):
|
134
|
+
return Err(f"Missing one of: {LOCATION}, {NUM_SIGNALS}, {SOLVENT}")
|
135
|
+
|
136
|
+
for pos, val in report_labels.items():
|
86
137
|
if val == "Location":
|
87
138
|
vial_location = df_labels[1][pos]
|
88
139
|
elif "Solvent" in val:
|
@@ -91,18 +142,35 @@ class CSVProcessor(ReportProcessor):
|
|
91
142
|
elif val == "Number of Signals":
|
92
143
|
num_signals = int(df_labels[1][pos])
|
93
144
|
for s in range(1, num_signals + 1):
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
145
|
+
try:
|
146
|
+
df = pd.read_csv(
|
147
|
+
os.path.join(self.path, f"{prefix}0{s}.CSV"),
|
148
|
+
encoding="utf-16",
|
149
|
+
header=None,
|
150
|
+
)
|
151
|
+
peaks = df.apply(lambda row: AgilentPeak(*row), axis=1)
|
152
|
+
except EmptyDataError:
|
153
|
+
peaks = []
|
154
|
+
try:
|
155
|
+
wavelength = df_labels[1][pos + s].partition(",4 Ref=off")[
|
156
|
+
0
|
157
|
+
][-3:]
|
158
|
+
signals[int(wavelength)] = list(peaks)
|
159
|
+
except (IndexError, ValueError):
|
160
|
+
# TODO: Ask about the MS signals
|
161
|
+
pass
|
99
162
|
break
|
100
163
|
|
101
|
-
return Ok(
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
164
|
+
return Ok(
|
165
|
+
AgilentReport(
|
166
|
+
signals=[
|
167
|
+
Signals(wavelength=w, peaks=s, data=None)
|
168
|
+
for w, s in signals.items()
|
169
|
+
],
|
170
|
+
vial_location=FiftyFourVialPlate.from_int(int(vial_location)),
|
171
|
+
solvents=solvents,
|
172
|
+
)
|
173
|
+
)
|
106
174
|
|
107
175
|
return Err("No report found")
|
108
176
|
|
@@ -111,34 +179,39 @@ class TXTProcessor(ReportProcessor):
|
|
111
179
|
"""
|
112
180
|
Regex matches for column and unit combinations, courtesy of Veronica Lai.
|
113
181
|
"""
|
182
|
+
|
114
183
|
_column_re_dictionary = {
|
115
|
-
|
116
|
-
|
184
|
+
"Peak": { # peak index
|
185
|
+
"#": "[ ]+(?P<Peak>[\d]+)", # number
|
117
186
|
},
|
118
|
-
|
119
|
-
|
187
|
+
"RetTime": { # retention time
|
188
|
+
"[min]": "(?P<RetTime>[\d]+.[\d]+)", # minutes
|
120
189
|
},
|
121
|
-
|
122
|
-
|
190
|
+
"Type": { # peak type
|
191
|
+
"": "(?P<Type>[A-Z]{1,3}(?: [A-Z]{1,2})*)", # todo this is different from <4.8.8 aghplc tools
|
123
192
|
},
|
124
|
-
|
125
|
-
|
193
|
+
"Width": { # peak width
|
194
|
+
"[min]": "(?P<Width>[\d]+.[\d]+[e+-]*[\d]+)",
|
126
195
|
},
|
127
|
-
|
128
|
-
|
129
|
-
|
196
|
+
"Area": { # peak area
|
197
|
+
"[mAU*s]": "(?P<Area>[\d]+.[\d]+[e+-]*[\d]+)", # area units
|
198
|
+
"%": "(?P<percent>[\d]+.[\d]+[e+-]*[\d]+)", # percent
|
130
199
|
},
|
131
|
-
|
132
|
-
|
200
|
+
"Height": { # peak height
|
201
|
+
"[mAU]": "(?P<Height>[\d]+.[\d]+[e+-]*[\d]+)",
|
133
202
|
},
|
134
|
-
|
135
|
-
|
203
|
+
"Name": {
|
204
|
+
"": "(?P<Name>[^\s]+(?:\s[^\s]+)*)", # peak name
|
136
205
|
},
|
137
206
|
}
|
138
207
|
|
139
|
-
def __init__(
|
140
|
-
|
141
|
-
|
208
|
+
def __init__(
|
209
|
+
self,
|
210
|
+
path: str,
|
211
|
+
min_ret_time: int = 0,
|
212
|
+
max_ret_time: int = 999,
|
213
|
+
target_wavelength_range=None,
|
214
|
+
):
|
142
215
|
"""
|
143
216
|
Class to process reports in CSV form.
|
144
217
|
|
@@ -147,12 +220,14 @@ class TXTProcessor(ReportProcessor):
|
|
147
220
|
:param max_ret_time: peaks will only be returned up to this time (min)
|
148
221
|
:param target_wavelength_range: range of wavelengths to return
|
149
222
|
"""
|
223
|
+
if target_wavelength_range is None:
|
224
|
+
target_wavelength_range = list(range(200, 300))
|
150
225
|
self.target_wavelength_range = target_wavelength_range
|
151
226
|
self.min_ret_time = min_ret_time
|
152
227
|
self.max_ret_time = max_ret_time
|
153
228
|
super().__init__(path)
|
154
229
|
|
155
|
-
def process_report(self) -> Result[AgilentReport, AnyStr]:
|
230
|
+
def process_report(self) -> Result[AgilentReport, Union[AnyStr, Exception]]:
|
156
231
|
"""
|
157
232
|
Method to parse details from CSV report.
|
158
233
|
If you want more functionality, use `aghplctools`.
|
@@ -162,34 +237,48 @@ class TXTProcessor(ReportProcessor):
|
|
162
237
|
:return: subset of complete report details, specifically the sample location, solvents in pumps,
|
163
238
|
and list of peaks at each wavelength channel.
|
164
239
|
"""
|
165
|
-
|
166
|
-
with open(os.path.join(self.path, "REPORT.TXT"), 'r', encoding='utf-16') as openfile:
|
167
|
-
text = openfile.read()
|
168
|
-
|
169
240
|
try:
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
241
|
+
with open(
|
242
|
+
os.path.join(self.path, "REPORT.TXT"), "r", encoding="utf-16"
|
243
|
+
) as openfile:
|
244
|
+
text = openfile.read()
|
245
|
+
|
246
|
+
try:
|
247
|
+
signals = self.parse_area_report(text)
|
248
|
+
except ValueError as e:
|
249
|
+
return Err("No peaks found: " + str(e))
|
250
|
+
|
251
|
+
signals = {
|
252
|
+
key: signals[key]
|
253
|
+
for key in self.target_wavelength_range
|
254
|
+
if key in signals
|
255
|
+
}
|
256
|
+
|
257
|
+
parsed_signals = []
|
258
|
+
for wavelength, wavelength_dict in signals.items():
|
259
|
+
current_wavelength_signals = Signals(
|
260
|
+
wavelength=int(wavelength), peaks=[], data=None
|
261
|
+
)
|
262
|
+
for ret_time, ret_time_dict in wavelength_dict.items():
|
263
|
+
if self.min_ret_time <= ret_time <= self.max_ret_time:
|
264
|
+
current_wavelength_signals.peaks.append(
|
265
|
+
AgilentPeak(
|
266
|
+
retention_time=ret_time,
|
267
|
+
area=ret_time_dict["Area"],
|
268
|
+
width=ret_time_dict["Width"],
|
269
|
+
height=ret_time_dict["Height"],
|
270
|
+
peak_number=None,
|
271
|
+
peak_type=ret_time_dict["Type"],
|
272
|
+
area_percent=None,
|
273
|
+
)
|
274
|
+
)
|
275
|
+
parsed_signals.append(current_wavelength_signals)
|
276
|
+
|
277
|
+
return Ok(
|
278
|
+
AgilentReport(vial_location=None, solvents=None, signals=parsed_signals)
|
279
|
+
)
|
280
|
+
except Exception as e:
|
281
|
+
return Err(e)
|
193
282
|
|
194
283
|
def parse_area_report(self, report_text: str) -> Dict:
|
195
284
|
"""
|
@@ -205,9 +294,9 @@ class TXTProcessor(ReportProcessor):
|
|
205
294
|
should be able to use the `parse_area_report` method of aghplctools v4.8.8
|
206
295
|
"""
|
207
296
|
if re.search(_no_peaks_re, report_text): # There are no peaks in Report.txt
|
208
|
-
raise ValueError(
|
297
|
+
raise ValueError("No peaks found in Report.txt")
|
209
298
|
blocks = _header_block_re.split(report_text)
|
210
|
-
signals = {} # output dictionary
|
299
|
+
signals: Dict[int, dict] = {} # output dictionary
|
211
300
|
for ind, block in enumerate(blocks):
|
212
301
|
# area report block
|
213
302
|
if _area_report_re.match(block): # match area report block
|
@@ -218,65 +307,75 @@ class TXTProcessor(ReportProcessor):
|
|
218
307
|
si = _signal_info_re.match(table)
|
219
308
|
if si is not None:
|
220
309
|
# some error state (e.g. 'not found')
|
221
|
-
if si.group(
|
310
|
+
if si.group("error") != "":
|
222
311
|
continue
|
223
|
-
wavelength =
|
312
|
+
wavelength = int(si.group("wavelength"))
|
224
313
|
if wavelength in signals:
|
225
314
|
# placeholder error raise just in case (this probably won't happen)
|
226
315
|
raise KeyError(
|
227
|
-
f
|
316
|
+
f"The wavelength {float(si.group('wavelength'))} is already in the signals dictionary"
|
317
|
+
)
|
228
318
|
signals[wavelength] = {}
|
229
319
|
# build peak regex
|
230
320
|
peak_re = self.build_peak_regex(table)
|
231
|
-
if
|
321
|
+
if (
|
322
|
+
peak_re is None
|
323
|
+
): # if there are no columns (empty table), continue
|
232
324
|
continue
|
233
|
-
for line in table.split(
|
325
|
+
for line in table.split("\n"):
|
234
326
|
peak = peak_re.match(line)
|
235
327
|
if peak is not None:
|
236
|
-
signals[wavelength][float(peak.group(
|
237
|
-
current = signals[wavelength][
|
328
|
+
signals[wavelength][float(peak.group("RetTime"))] = {}
|
329
|
+
current = signals[wavelength][
|
330
|
+
float(peak.group("RetTime"))
|
331
|
+
]
|
238
332
|
for key in self._column_re_dictionary:
|
239
333
|
if key in peak.re.groupindex:
|
240
334
|
try: # try float conversion, otherwise continue
|
241
|
-
|
335
|
+
current[key] = float(peak.group(key))
|
242
336
|
except ValueError:
|
243
|
-
|
244
|
-
current[key] = value
|
337
|
+
current[key] = peak.group(key)
|
245
338
|
else: # ensures defined
|
246
339
|
current[key] = None
|
247
340
|
return signals
|
248
341
|
|
249
|
-
def build_peak_regex(self, signal_table: str) -> Pattern[
|
342
|
+
def build_peak_regex(self, signal_table: str) -> Pattern[str] | None:
|
250
343
|
"""
|
251
344
|
Builds a peak regex from a signal table. Courtesy of Veronica Lai.
|
252
345
|
|
253
346
|
:param signal_table: block of lines associated with an area table
|
254
347
|
:return: peak line regex object (<=3.6 _sre.SRE_PATTERN, >=3.7 re.Pattern)
|
255
348
|
"""
|
256
|
-
split_table = signal_table.split(
|
349
|
+
split_table = signal_table.split("\n")
|
257
350
|
if len(split_table) <= 4: # catch peak table with no values
|
258
351
|
return None
|
259
352
|
# todo verify that these indicies are always true
|
260
353
|
column_line = split_table[2] # table column line
|
261
354
|
unit_line = split_table[3] # column unit line
|
262
|
-
length_line = [len(val) + 1 for val in split_table[4].split(
|
355
|
+
length_line = [len(val) + 1 for val in split_table[4].split("|")] # length line
|
263
356
|
|
264
357
|
# iterate over header values and units to build peak table regex
|
265
358
|
peak_re_string = []
|
266
359
|
for header, unit in zip(
|
267
|
-
|
268
|
-
chunk_string(unit_line, length_line)
|
360
|
+
chunk_string(column_line, length_line), chunk_string(unit_line, length_line)
|
269
361
|
):
|
270
|
-
if header ==
|
362
|
+
if header == "": # todo create a better catch for an undefined header
|
271
363
|
continue
|
272
364
|
try:
|
273
365
|
peak_re_string.append(
|
274
|
-
self._column_re_dictionary[header][
|
366
|
+
self._column_re_dictionary[header][
|
367
|
+
unit
|
368
|
+
] # append the appropriate regex
|
275
369
|
)
|
276
370
|
except KeyError: # catch for undefined regexes (need to be built)
|
277
|
-
raise KeyError(
|
278
|
-
|
371
|
+
raise KeyError(
|
372
|
+
f'The header/unit combination "{header}" "{unit}" is not defined in the peak regex '
|
373
|
+
f"dictionary. Let Lars know."
|
374
|
+
)
|
375
|
+
|
279
376
|
return re.compile(
|
280
|
-
|
281
|
-
|
377
|
+
"[ ]+".join(
|
378
|
+
peak_re_string
|
379
|
+
) # constructed string delimited by 1 or more spaces
|
380
|
+
+ "[\s]*" # and any remaining white space
|
282
381
|
)
|
@@ -3,11 +3,6 @@
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
from .comm import CommunicationController
|
6
|
-
from .
|
7
|
-
from .tables.sequence import SequenceController
|
6
|
+
from . import tables
|
8
7
|
|
9
|
-
__all__ = [
|
10
|
-
'CommunicationController',
|
11
|
-
'MethodController',
|
12
|
-
'SequenceController'
|
13
|
-
]
|
8
|
+
__all__ = ["CommunicationController", "tables"]
|