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,822 @@
|
|
1
|
+
"""Module for NMR spectral data loading and manipulating"""
|
2
|
+
|
3
|
+
# pylint: disable=attribute-defined-outside-init
|
4
|
+
import os
|
5
|
+
import logging
|
6
|
+
import time
|
7
|
+
from typing import Union
|
8
|
+
|
9
|
+
import numpy as np
|
10
|
+
import scipy
|
11
|
+
import nmrglue as ng
|
12
|
+
import matplotlib.pyplot as plt
|
13
|
+
|
14
|
+
from ....analysis import AbstractSpectrum
|
15
|
+
from ....analysis.spec_utils import *
|
16
|
+
|
17
|
+
# standard filenames for spectral data
|
18
|
+
FID_DATA = "data.1d"
|
19
|
+
ACQUISITION_PARAMETERS = "acqu.par"
|
20
|
+
PROCESSED_SPECTRUM = "spectrum_processed.1d" # not always present
|
21
|
+
PROTOCOL_PARAMETERS = "protocol.par"
|
22
|
+
|
23
|
+
# format used in acquisition parameters
|
24
|
+
TIME_FORMAT = r"%Y-%m-%dT%H:%M:%S.%f"
|
25
|
+
|
26
|
+
# reserved for future use
|
27
|
+
JCAMP_DX_SPECTRUM = "nmr_fid.dx"
|
28
|
+
CSV_SPECTRUM = "spectrum.csv"
|
29
|
+
|
30
|
+
# filename for shimming parameters
|
31
|
+
SHIMMING_PARAMETERS = "shim.par"
|
32
|
+
SHIMMING_FID = "sample_fid.1d"
|
33
|
+
SHIMMING_SPECTRUM = "spectrum.1d"
|
34
|
+
|
35
|
+
|
36
|
+
class SpinsolveNMRSpectrum(AbstractSpectrum):
|
37
|
+
"""Class for NMR spectrum loading and handling."""
|
38
|
+
|
39
|
+
AXIS_MAPPING = {"x": "time", "y": ""}
|
40
|
+
|
41
|
+
INTERNAL_PROPERTIES = {
|
42
|
+
"baseline",
|
43
|
+
"data_path",
|
44
|
+
"_uc",
|
45
|
+
}
|
46
|
+
|
47
|
+
def __init__(self, path=None, autosaving=False):
|
48
|
+
|
49
|
+
if path is None:
|
50
|
+
path = os.path.join(".", "nmr_data")
|
51
|
+
|
52
|
+
self.logger = logging.getLogger("spinsolve.spectrum")
|
53
|
+
|
54
|
+
# updating public properties to include the universal dictionary
|
55
|
+
self.PUBLIC_PROPERTIES.add("udic")
|
56
|
+
# and parameters
|
57
|
+
self.PUBLIC_PROPERTIES.add("parameters")
|
58
|
+
|
59
|
+
# autosaving set to False, since spectra are saved by Spinsolve anyway
|
60
|
+
super().__init__(path, autosaving)
|
61
|
+
|
62
|
+
# universal dictionary for the acquisition parameters
|
63
|
+
# placeholder, will be updated when spectral data is loaded
|
64
|
+
self.udic = ng.fileio.fileiobase.create_blank_udic(1) # 1D spectrum
|
65
|
+
|
66
|
+
# placeholder to store shimming parameters in the current session
|
67
|
+
self.last_shimming_time = None
|
68
|
+
self.last_shimming_results = None
|
69
|
+
|
70
|
+
def load_spectrum(self, data_path, start_time=None, preprocessed=True):
|
71
|
+
"""Loads the spectra from the given folder.
|
72
|
+
|
73
|
+
If preprocessed argument is True, loading the spectral data already
|
74
|
+
processed by the Spinsolve software (fft + autophasing).
|
75
|
+
|
76
|
+
Args:
|
77
|
+
data_path (str): Path where NMR data has been saved.
|
78
|
+
start_time (float, optional): Start time of the current experiment,
|
79
|
+
used to calculate the timestamp for the spectrum. If omitted,
|
80
|
+
uses the time since Epoch from the spectrum acquisition
|
81
|
+
parameters.
|
82
|
+
preprocessed (bool, optional): If True - will load preprocessed (by
|
83
|
+
Spinsolve software) spectral data. If False (default) - base fid
|
84
|
+
is loaded and used for further processing.
|
85
|
+
"""
|
86
|
+
|
87
|
+
# this is needed to avoid dropping parameters when called in parent
|
88
|
+
# class, since _dump() is called there as well
|
89
|
+
if self.x is not None:
|
90
|
+
if self.autosaving:
|
91
|
+
self.save_data()
|
92
|
+
self._dump()
|
93
|
+
|
94
|
+
# filepaths
|
95
|
+
param_path = os.path.join(data_path, ACQUISITION_PARAMETERS)
|
96
|
+
processed_path = os.path.join(data_path, PROCESSED_SPECTRUM)
|
97
|
+
fid_path = os.path.join(data_path, FID_DATA)
|
98
|
+
|
99
|
+
# loading parameters
|
100
|
+
try:
|
101
|
+
self.parameters = self.extract_parameters(param_path)
|
102
|
+
except FileNotFoundError:
|
103
|
+
# this happens only if shimming was performed
|
104
|
+
shim_path = os.path.join(data_path, SHIMMING_PARAMETERS)
|
105
|
+
self.parameters = self.extract_parameters(shim_path)
|
106
|
+
|
107
|
+
# updating placeholders
|
108
|
+
self.last_shimming_results = {
|
109
|
+
parameter: self.parameters[parameter]
|
110
|
+
for parameter in self.parameters
|
111
|
+
if parameter.startswith("shim")
|
112
|
+
}
|
113
|
+
|
114
|
+
# updating last shimming time
|
115
|
+
self.last_shimming_time = time.strptime(
|
116
|
+
self.parameters["CurrentTime"], TIME_FORMAT
|
117
|
+
)
|
118
|
+
|
119
|
+
# updating file names for the shimming
|
120
|
+
processed_path = os.path.join(data_path, SHIMMING_SPECTRUM)
|
121
|
+
|
122
|
+
# forcing preprocessed to deal with frequency domain not raw FID
|
123
|
+
preprocessed = True
|
124
|
+
|
125
|
+
self.data_path = data_path
|
126
|
+
|
127
|
+
# extracting the time from acquisition parameters
|
128
|
+
spectrum_time = time.strptime(self.parameters["CurrentTime"], TIME_FORMAT)
|
129
|
+
|
130
|
+
if start_time is not None:
|
131
|
+
timestamp = round(time.mktime(spectrum_time) - start_time)
|
132
|
+
else:
|
133
|
+
timestamp = round(time.mktime(spectrum_time))
|
134
|
+
|
135
|
+
# loading raw fid data
|
136
|
+
if not preprocessed:
|
137
|
+
x_axis, y_real, y_img = self.extract_data(fid_path)
|
138
|
+
spectrum_data = np.array(y_real + y_img * 1j)
|
139
|
+
|
140
|
+
# updating the universal dictionary
|
141
|
+
self.udic[0].update(
|
142
|
+
# spectral width in kHz
|
143
|
+
sw=self.parameters["bandwidth"] * 1e3,
|
144
|
+
# carrier frequency
|
145
|
+
car=self.parameters["bandwidth"] * 1e3 / 2
|
146
|
+
+ self.parameters["lowestFrequency"],
|
147
|
+
# observed frequency
|
148
|
+
obs=self.parameters["b1Freq"],
|
149
|
+
# number of points
|
150
|
+
size=self.parameters["nrPnts"],
|
151
|
+
# time domain
|
152
|
+
time=True,
|
153
|
+
# label, e.g. 1H
|
154
|
+
label=self.parameters["rxChannel"],
|
155
|
+
)
|
156
|
+
|
157
|
+
# changing axis mapping according to raw FID
|
158
|
+
self.AXIS_MAPPING.update(x="time")
|
159
|
+
|
160
|
+
# creating unit conversion object
|
161
|
+
self._uc = ng.fileio.fileiobase.uc_from_udic(self.udic)
|
162
|
+
|
163
|
+
# check for the preprocessed file, as it's not always present
|
164
|
+
elif os.path.isfile(processed_path) and preprocessed:
|
165
|
+
# loading frequency axis and real part of the complex spectrum data
|
166
|
+
x_axis, spectrum_data, _ = self.extract_data(processed_path)
|
167
|
+
# reversing spectrum order to match default nmr order
|
168
|
+
# i.e. highest - left
|
169
|
+
x_axis = x_axis[::-1]
|
170
|
+
spectrum_data = spectrum_data[::-1]
|
171
|
+
# updating axis mapping
|
172
|
+
self.AXIS_MAPPING.update(x="ppm")
|
173
|
+
|
174
|
+
else:
|
175
|
+
self.logger.warning(
|
176
|
+
"Current version of SpinsolveNMRSpectrum does \
|
177
|
+
not support raw FID data and no processed spectrum was found. Please change \
|
178
|
+
settings of the Spinsolve Software to enable default processing"
|
179
|
+
)
|
180
|
+
raise AttributeError(
|
181
|
+
f"Processed spectrum was not found in the \
|
182
|
+
supplied directory <{data_path}>"
|
183
|
+
)
|
184
|
+
|
185
|
+
# loading all data
|
186
|
+
super().load_spectrum(x_axis, spectrum_data, int(timestamp))
|
187
|
+
|
188
|
+
### PUBLIC METHODS TO LOAD RAW DATA ###
|
189
|
+
|
190
|
+
def extract_data(self, spectrum_path):
|
191
|
+
"""Reads the Spinsolve spectrum file and extracts the spectrum data
|
192
|
+
from it.
|
193
|
+
|
194
|
+
Data is stored in binary format as C struct data type. First 32 bytes
|
195
|
+
(8 integers * 4 bytes) contains the header information and can be
|
196
|
+
discarded. The rest data is stored as float (4 byte) data points for X
|
197
|
+
axis and complex number (as float, float for real and imaginary parts)
|
198
|
+
data points for Y axis.
|
199
|
+
|
200
|
+
Refer to the software manual (v 1.16, July 2019 Magritek) for details.
|
201
|
+
|
202
|
+
Args:
|
203
|
+
spectrum_path: Path to saved NMR spectrum data.
|
204
|
+
|
205
|
+
Returns:
|
206
|
+
tuple[x_axis, y_data_real, y_data_img]:
|
207
|
+
x_axis (:obj: np.array, dtype='float32'): X axis points.
|
208
|
+
y_data_real (:obj: np.array, dtype='float32'): real part of the
|
209
|
+
complex spectrum data.
|
210
|
+
y_data_img (:obj: np.array, dtype='float32'): imaginary part of
|
211
|
+
the complex spectrum data.
|
212
|
+
|
213
|
+
"""
|
214
|
+
|
215
|
+
# reading data with numpy fromfile
|
216
|
+
# the header is discarded
|
217
|
+
spectrum_data = np.fromfile(spectrum_path, dtype="<f")[8:]
|
218
|
+
|
219
|
+
x_axis = spectrum_data[: len(spectrum_data) // 3]
|
220
|
+
|
221
|
+
# breaking the rest of the data into real and imaginary part
|
222
|
+
y_real = spectrum_data[len(spectrum_data) // 3 :][::2]
|
223
|
+
y_img = spectrum_data[len(spectrum_data) // 3 :][1::2]
|
224
|
+
|
225
|
+
return (x_axis, y_real, y_img)
|
226
|
+
|
227
|
+
def extract_parameters(self, params_path):
|
228
|
+
"""Get the NMR parameters from the given folder.
|
229
|
+
|
230
|
+
Args:
|
231
|
+
params_path (str): Path to saved NMR data.
|
232
|
+
|
233
|
+
Returns:
|
234
|
+
Dict: Acquisition parameters.
|
235
|
+
"""
|
236
|
+
|
237
|
+
# loading spectrum parameters
|
238
|
+
spec_params = {}
|
239
|
+
with open(params_path) as fileobj:
|
240
|
+
param = fileobj.readline()
|
241
|
+
while param:
|
242
|
+
# in form of "Param" = "Value"\n
|
243
|
+
parameter, value = param.split("=", maxsplit=1)
|
244
|
+
# stripping from whitespaces, newlines and extra doublequotes
|
245
|
+
parameter = parameter.strip()
|
246
|
+
value = value.strip(' \n"')
|
247
|
+
# special case: userData
|
248
|
+
# converting to nested dict
|
249
|
+
if parameter == "userData" and value:
|
250
|
+
values = value.split(";")
|
251
|
+
value = {}
|
252
|
+
for key_value in values:
|
253
|
+
key, val = key_value.split("=")
|
254
|
+
value[key] = val
|
255
|
+
# converting values to float if possible
|
256
|
+
try:
|
257
|
+
spec_params[parameter] = float(value)
|
258
|
+
except (ValueError, TypeError):
|
259
|
+
spec_params[parameter] = value
|
260
|
+
param = fileobj.readline()
|
261
|
+
|
262
|
+
return spec_params
|
263
|
+
|
264
|
+
def show_spectrum(
|
265
|
+
self,
|
266
|
+
filename=None,
|
267
|
+
title=None,
|
268
|
+
label=None,
|
269
|
+
):
|
270
|
+
"""Plots the spectral data using matplotlib.pyplot module.
|
271
|
+
|
272
|
+
Redefined from ancestor class to support axis inverting.
|
273
|
+
|
274
|
+
Args:
|
275
|
+
filename (str, optional): Filename for the current plot. If omitted,
|
276
|
+
file is not saved.
|
277
|
+
title (str, optional): Title for the spectrum plot. If omitted, no
|
278
|
+
title is set.
|
279
|
+
label (str, optional): Label for the spectrum plot. If omitted, uses
|
280
|
+
the spectrum timestamp.
|
281
|
+
"""
|
282
|
+
if label is None:
|
283
|
+
label = f"{self.timestamp}"
|
284
|
+
|
285
|
+
fig, ax = plt.subplots(figsize=(12, 8))
|
286
|
+
|
287
|
+
ax.plot(
|
288
|
+
self.x,
|
289
|
+
self.y,
|
290
|
+
color="xkcd:navy blue",
|
291
|
+
label=label,
|
292
|
+
)
|
293
|
+
|
294
|
+
ax.set_xlabel(self.AXIS_MAPPING["x"])
|
295
|
+
ax.set_ylabel(self.AXIS_MAPPING["y"])
|
296
|
+
|
297
|
+
if title is not None:
|
298
|
+
ax.set_title(title)
|
299
|
+
|
300
|
+
# plotting peaks if found
|
301
|
+
if self.peaks is not None:
|
302
|
+
plt.scatter(
|
303
|
+
self.peaks[:, 1],
|
304
|
+
self.peaks[:, 2],
|
305
|
+
label="found peaks",
|
306
|
+
color="xkcd:tangerine",
|
307
|
+
)
|
308
|
+
|
309
|
+
ax.legend()
|
310
|
+
|
311
|
+
# inverting if ppm scale
|
312
|
+
if self.AXIS_MAPPING["x"] == "ppm":
|
313
|
+
ax.invert_xaxis()
|
314
|
+
|
315
|
+
if filename is None:
|
316
|
+
fig.show()
|
317
|
+
|
318
|
+
else:
|
319
|
+
path = os.path.join(self.path, "images")
|
320
|
+
os.makedirs(path, exist_ok=True)
|
321
|
+
fig.savefig(os.path.join(path, f"{filename}.png"), dpi=150)
|
322
|
+
|
323
|
+
def fft(self, in_place=True):
|
324
|
+
"""Fourier transformation, NMR ordering of the results.
|
325
|
+
|
326
|
+
This is the wrapper around nmrglue.process.proc_base.fft function.
|
327
|
+
Please refer to original function documentation for details.
|
328
|
+
|
329
|
+
Args:
|
330
|
+
in_place(bool, optional): If True (default), self.y is updated;
|
331
|
+
returns new array if False.
|
332
|
+
|
333
|
+
Returns:
|
334
|
+
Union[:np.array:, None]: If in_place is True, will return new array
|
335
|
+
after FFT.
|
336
|
+
"""
|
337
|
+
|
338
|
+
if in_place:
|
339
|
+
self.y = ng.proc_base.fft(self.y)
|
340
|
+
|
341
|
+
# updating x and y axis
|
342
|
+
self.AXIS_MAPPING.update(x="ppm")
|
343
|
+
self.x = self._uc.ppm_scale()
|
344
|
+
|
345
|
+
# updating the udic to frequency domain
|
346
|
+
self.udic[0]["time"] = False
|
347
|
+
self.udic[0]["freq"] = True
|
348
|
+
|
349
|
+
else:
|
350
|
+
return ng.proc_base.fft(self.y)
|
351
|
+
|
352
|
+
def autophase(self, in_place=True, function="peak_minima", p0=0.0, p1=0.0):
|
353
|
+
"""Automatic linear phase correction. FFT is performed!
|
354
|
+
|
355
|
+
This is the wrapper around nmrglue.process.proc_autophase.autops
|
356
|
+
function. Please refer to original function documentation for details.
|
357
|
+
|
358
|
+
Args:
|
359
|
+
in_place (bool, optional): If True (default), self.y is updated;
|
360
|
+
returns new array if False.
|
361
|
+
function (Union[str, Callable], optional): Algorithm to use for
|
362
|
+
phase scoring. Builtin functions can be specified by one of the
|
363
|
+
following strings: "acme" or "peak_minima". This refers to
|
364
|
+
nmrglue.process.proc_autophase.autops function, "peak_minima"
|
365
|
+
(default) was found to perform best.
|
366
|
+
p0 (float, optional): Initial zero order phase in degrees.
|
367
|
+
p1 (float, optional): Initial first order phase in degrees.
|
368
|
+
|
369
|
+
Returns:
|
370
|
+
Union[:np.array:, None]: If in_place is True, will return new array
|
371
|
+
after phase correction.
|
372
|
+
"""
|
373
|
+
|
374
|
+
# check if fft was performed
|
375
|
+
if self.AXIS_MAPPING["x"] == "time":
|
376
|
+
self.fft()
|
377
|
+
|
378
|
+
autophased = ng.process.proc_autophase.autops(self.y, function, p0, p1)
|
379
|
+
|
380
|
+
if in_place:
|
381
|
+
self.y = autophased
|
382
|
+
return
|
383
|
+
|
384
|
+
return autophased
|
385
|
+
|
386
|
+
def correct_baseline(self, in_place=True, wd=20):
|
387
|
+
"""Automatic baseline correction using distribution based
|
388
|
+
classification method.
|
389
|
+
|
390
|
+
Algorithm described in: Wang et al. Anal. Chem. 2013, 85, 1231-1239
|
391
|
+
|
392
|
+
This is the wrapper around nmrglue.process.proc_bl.baseline_corrector
|
393
|
+
function. Please refer to original function documentation for details.
|
394
|
+
|
395
|
+
Args:
|
396
|
+
in_place(bool, optional): If True (default), self.y is updated;
|
397
|
+
returns new array if False.
|
398
|
+
wd(float, optional): Median window size in pts.
|
399
|
+
|
400
|
+
Returns:
|
401
|
+
Union[:np.array:, None]: If in_place is True, will return new array
|
402
|
+
after baseline correction.
|
403
|
+
"""
|
404
|
+
|
405
|
+
# check if fft was performed
|
406
|
+
if self.AXIS_MAPPING["x"] == "time":
|
407
|
+
self.fft()
|
408
|
+
|
409
|
+
corrected = ng.process.proc_bl.baseline_corrector(self.y, wd)
|
410
|
+
|
411
|
+
if in_place:
|
412
|
+
self.y = corrected
|
413
|
+
return
|
414
|
+
|
415
|
+
return corrected
|
416
|
+
|
417
|
+
def load_data(self, path):
|
418
|
+
# overwritten from abstract class to allow updating of unit conversion
|
419
|
+
super().load_data(path)
|
420
|
+
self._uc = ng.fileio.fileiobase.uc_from_udic(self.udic)
|
421
|
+
|
422
|
+
# updating axis mapping from "time" default
|
423
|
+
if self.udic[0]["freq"]:
|
424
|
+
self.AXIS_MAPPING.update(x="ppm")
|
425
|
+
|
426
|
+
elif self.udic[0]["time"]:
|
427
|
+
self.AXIS_MAPPING.update(x="time")
|
428
|
+
|
429
|
+
def smooth_spectrum(self, in_place=True, routine="ng", **params):
|
430
|
+
"""Smoothes the spectrum.
|
431
|
+
|
432
|
+
Depending on the routine chosen will use either Savitsky-Golay filter
|
433
|
+
from scipy.signal module or nmrglue custom function.
|
434
|
+
|
435
|
+
!Note: savgol will cast complex dtype to float!
|
436
|
+
|
437
|
+
Args:
|
438
|
+
in_place(bool, optional): If True (default), self.y is updated;
|
439
|
+
returns new array if False.
|
440
|
+
|
441
|
+
routine(str, optional): Smoothing routine.
|
442
|
+
"ng" (default) will use custom smoothing function from
|
443
|
+
nmrglue.process.proc_base module.
|
444
|
+
|
445
|
+
"savgol" will use savgol_filter method from scipt.signal module
|
446
|
+
defined in ancestor method.
|
447
|
+
|
448
|
+
parram in params: Keyword arguments for the chosen routine function.
|
449
|
+
For "savgol" routine:
|
450
|
+
|
451
|
+
window_length (int): The length of the filter window (i.e.
|
452
|
+
thenumber of coefficients). window_length must be a
|
453
|
+
positive odd integer.
|
454
|
+
|
455
|
+
polyorder (int): The order of the polynomial used to fit the
|
456
|
+
samples. polyorder must be less than window_length.
|
457
|
+
|
458
|
+
For "ng" routine:
|
459
|
+
|
460
|
+
n (int): Size of smoothing windows (+/- points).
|
461
|
+
|
462
|
+
Returns:
|
463
|
+
Union[:np.array:, None]: If in_place is True, will return new array
|
464
|
+
after baseline correction.
|
465
|
+
"""
|
466
|
+
|
467
|
+
if routine == "savgol":
|
468
|
+
super().smooth_spectrum(in_place=in_place, **params)
|
469
|
+
|
470
|
+
elif routine == "ng":
|
471
|
+
# using default value
|
472
|
+
if not params:
|
473
|
+
params = {"n": 5}
|
474
|
+
|
475
|
+
if in_place:
|
476
|
+
self.y = ng.process.proc_base.smo(self.y, **params)
|
477
|
+
return
|
478
|
+
|
479
|
+
return ng.process.proc_base.smo(self.y, **params)
|
480
|
+
|
481
|
+
else:
|
482
|
+
raise ValueError(
|
483
|
+
'Please choose either nmrglue ("ng") or Savitsky-\
|
484
|
+
Golay ("savgol") smoothing routine'
|
485
|
+
)
|
486
|
+
|
487
|
+
def apodization(self, in_place=True, function="em", **params):
|
488
|
+
"""Applies a chosen window function.
|
489
|
+
|
490
|
+
Args:
|
491
|
+
in_place (bool, optional): If True (default), self.y is updated;
|
492
|
+
returns new array if False.
|
493
|
+
|
494
|
+
function (str, optional): Window function of choice.
|
495
|
+
"em" - exponential multiply window (mimic NMRPipe EM function).
|
496
|
+
"gm" - Lorentz-to-Gauss window function (NMRPipe GM function).
|
497
|
+
"gmb" - Gauss-like window function (NMRPipe GMB function).
|
498
|
+
|
499
|
+
param in params: Keyword arguments for the chosen window function:
|
500
|
+
|
501
|
+
For "em":
|
502
|
+
See reference for nmrglue.proc_base.em and NMRPipe EM
|
503
|
+
functions.
|
504
|
+
|
505
|
+
lb (float): Exponential decay of the window in terms of a
|
506
|
+
line broadening in Hz. Negative values will generate an
|
507
|
+
increasing exponential window, which corresponds to a
|
508
|
+
line sharpening. The line-broadening parameter is often
|
509
|
+
selected to match the natural linewidth.
|
510
|
+
|
511
|
+
For "gm":
|
512
|
+
See reference for nmrglue.proc_base.gm and NMRPipe GM
|
513
|
+
functions.
|
514
|
+
|
515
|
+
g1 (float): Specifies the inverse exponential to apply in
|
516
|
+
terms of a line sharpening in Hz. It is usually adjusted
|
517
|
+
to match the natural linewidth. The default value is
|
518
|
+
0.0, which means no exponential term will be applied,
|
519
|
+
and the window will be a pure Gaussian function.
|
520
|
+
|
521
|
+
g2 (float): Specifies the Gaussian to apply in terms of a
|
522
|
+
line broadening in Hz. It is usually adjusted to be
|
523
|
+
larger (x 1.25 - 4.0) than the line sharpening specified
|
524
|
+
by the g1 attribute.
|
525
|
+
|
526
|
+
g3 (float): Specifies the position of the Gaussian
|
527
|
+
function's maximum on the FID. It is specified as a
|
528
|
+
value ranging from 0.0 (Gaussian maximum at the first
|
529
|
+
point of the FID) to 1.0 (Gaussian maximum at the last
|
530
|
+
point of the FID). It most applications, the default
|
531
|
+
value of 0.0 is used.
|
532
|
+
|
533
|
+
For "gmb":
|
534
|
+
See reference for nmrglue.proc_base.gmb and NMRPipe GMB
|
535
|
+
functions.
|
536
|
+
|
537
|
+
lb (float): Specifies an exponential factor in the chosen
|
538
|
+
Gauss window function. This value is usually specified
|
539
|
+
as a negative number which is about the same size as the
|
540
|
+
natural linewidth in Hz. The default value is 0.0, which
|
541
|
+
means no exponential term will be applied.
|
542
|
+
|
543
|
+
gb (float): Specifies a Gaussian factor gb, as used in the
|
544
|
+
chosen Gauss window function. It is usually specified as
|
545
|
+
a positive number which is a fraction of 1.0. The
|
546
|
+
default value is 0.0, which leads to an undefined window
|
547
|
+
function according to the formula; for this reason, the
|
548
|
+
Gaussian term is omitted from the calculation when gb
|
549
|
+
0.0 is given.
|
550
|
+
|
551
|
+
Returns:
|
552
|
+
Union[:np.array:, None]: If in_place is True, will return new array
|
553
|
+
after baseline correction.
|
554
|
+
"""
|
555
|
+
# TODO check for Fourier transformation!
|
556
|
+
|
557
|
+
if function == "em":
|
558
|
+
# converting lb value to NMRPipe-like
|
559
|
+
if "lb" in params:
|
560
|
+
# deviding by spectral width in Hz
|
561
|
+
params["lb"] = params["lb"] / self.udic[0]["sw"]
|
562
|
+
|
563
|
+
if in_place:
|
564
|
+
self.y = ng.process.proc_base.em(self.y, **params)
|
565
|
+
return
|
566
|
+
|
567
|
+
return ng.process.proc_base.em(self.y, **params)
|
568
|
+
|
569
|
+
elif function == "gm":
|
570
|
+
# converting values into NMRPipe-like
|
571
|
+
if "g1" in params:
|
572
|
+
params["g1"] = params["g1"] / self.udic[0]["sw"]
|
573
|
+
|
574
|
+
if "g2" in params:
|
575
|
+
params["g2"] = params["g2"] / self.udic[0]["sw"]
|
576
|
+
|
577
|
+
if in_place:
|
578
|
+
self.y = ng.process.proc_base.gm(self.y, **params)
|
579
|
+
return
|
580
|
+
|
581
|
+
return ng.process.proc_base.gm(self.y, **params)
|
582
|
+
|
583
|
+
elif function == "gmb":
|
584
|
+
# converting values into NMRPipe-like
|
585
|
+
# for formula reference see documentation and source code of
|
586
|
+
# nmrglue.proc_base.gmb function and NMRPipe GMB command reference
|
587
|
+
if "lb" in params:
|
588
|
+
a = np.pi * params["lb"] / self.udic[0]["sw"]
|
589
|
+
else:
|
590
|
+
a = 0.0
|
591
|
+
|
592
|
+
if "gb" in params:
|
593
|
+
b = -a / (2.0 * params["gb"] * self.udic[0]["size"])
|
594
|
+
else:
|
595
|
+
b = 0.0
|
596
|
+
|
597
|
+
if in_place:
|
598
|
+
self.y = ng.process.proc_base.gmb(self.y, a=a, b=b)
|
599
|
+
return
|
600
|
+
|
601
|
+
return ng.process.proc_base.gmb(self.y, a=a, b=b)
|
602
|
+
|
603
|
+
def zero_fill(self, n=1, in_place=True):
|
604
|
+
"""Zero filling the data by 2**n.
|
605
|
+
|
606
|
+
Args:
|
607
|
+
n (int): power of 2 to append 0 to the data.
|
608
|
+
in_place (bool, optional): If True (default), self.y is updated;
|
609
|
+
returns new array if False.
|
610
|
+
|
611
|
+
Returns:
|
612
|
+
Union[:np.array:, None]: If in_place is True, will return new array
|
613
|
+
after baseline correction.
|
614
|
+
"""
|
615
|
+
|
616
|
+
if in_place:
|
617
|
+
# zero fill is useless when fft performed
|
618
|
+
if self.AXIS_MAPPING["x"] == "ppm":
|
619
|
+
self.logger.warning(
|
620
|
+
"FFT already performed, zero filling \
|
621
|
+
skipped"
|
622
|
+
)
|
623
|
+
return
|
624
|
+
|
625
|
+
# extending y axis
|
626
|
+
self.y = ng.process.proc_base.zf_double(self.y, n)
|
627
|
+
|
628
|
+
# extending x axis
|
629
|
+
self.x = np.linspace(self.x[0], self.x[-1] * 2**n, self.y.shape[0])
|
630
|
+
|
631
|
+
# updating udic and uc
|
632
|
+
self.udic[0].update(size=self.x.size)
|
633
|
+
self._uc = ng.fileio.fileiobase.uc_from_udic(self.udic)
|
634
|
+
return
|
635
|
+
|
636
|
+
return ng.process.proc_base.zf_double(self.y, n)
|
637
|
+
|
638
|
+
def generate_peak_regions(
|
639
|
+
self,
|
640
|
+
magnitude=True,
|
641
|
+
derivative=True,
|
642
|
+
smoothed=True,
|
643
|
+
d_merge=0.056,
|
644
|
+
d_expand=0.0,
|
645
|
+
):
|
646
|
+
"""Generate regions if interest potentially containing compound peaks
|
647
|
+
from the spectral data.
|
648
|
+
|
649
|
+
Args:
|
650
|
+
d_merge (float, Optional): arbitrary interval (in ppm!) to merge
|
651
|
+
several regions, if the distance between is lower.
|
652
|
+
d_expand (float, Optional): arbitrary value (in ppm!) to expand the
|
653
|
+
regions after automatic assigning and filtering.
|
654
|
+
|
655
|
+
Returns:
|
656
|
+
:obj:np.array: 2D Mx2 array with peak regions indexes (rows) as left
|
657
|
+
and right borders (columns).
|
658
|
+
"""
|
659
|
+
|
660
|
+
# check if fft was performed
|
661
|
+
if self.AXIS_MAPPING["x"] != "ppm":
|
662
|
+
self.logger.warning("Please perform FFT first.")
|
663
|
+
return np.array([[]])
|
664
|
+
|
665
|
+
# placeholder
|
666
|
+
peak_map = np.full_like(self.x, False)
|
667
|
+
|
668
|
+
if magnitude:
|
669
|
+
# looking for peaks in magnitude mode
|
670
|
+
magnitude_spectrum = np.sqrt(self.y.real**2 + self.y.imag**2)
|
671
|
+
# mapping
|
672
|
+
peak_map = np.logical_or(
|
673
|
+
create_binary_peak_map(magnitude_spectrum), peak_map
|
674
|
+
)
|
675
|
+
else:
|
676
|
+
peak_map = np.logical_or(create_binary_peak_map(self.y), peak_map)
|
677
|
+
|
678
|
+
# additionally in the derivative
|
679
|
+
if derivative:
|
680
|
+
try:
|
681
|
+
derivative_map = create_binary_peak_map(np.gradient(magnitude_spectrum))
|
682
|
+
except NameError:
|
683
|
+
derivative_map = create_binary_peak_map(np.gradient(self.y))
|
684
|
+
# combining
|
685
|
+
peak_map = np.logical_or(derivative_map, peak_map)
|
686
|
+
|
687
|
+
# and in the smoothed version
|
688
|
+
if smoothed:
|
689
|
+
try:
|
690
|
+
smoothed = scipy.ndimage.gaussian_filter1d(magnitude_spectrum, 3)
|
691
|
+
except NameError:
|
692
|
+
# smoothing only supported on non-complex data
|
693
|
+
smoothed = scipy.ndimage.gaussian_filter1d(self.y.real, 3)
|
694
|
+
# combining
|
695
|
+
peak_map = np.logical_or(create_binary_peak_map(smoothed), peak_map)
|
696
|
+
|
697
|
+
# extracting the regions from the full map
|
698
|
+
regions = combine_map_to_regions(peak_map)
|
699
|
+
|
700
|
+
# Skip further steps if no peaks identified
|
701
|
+
if not regions.size > 0:
|
702
|
+
return regions
|
703
|
+
|
704
|
+
# filtering, merging, expanding
|
705
|
+
regions = filter_regions(self.x, regions)
|
706
|
+
regions = filter_noisy_regions(self.y, regions)
|
707
|
+
if d_merge:
|
708
|
+
regions = merge_regions(self.x, regions, d_merge=d_merge)
|
709
|
+
if d_expand:
|
710
|
+
regions = expand_regions(self.x, regions, d_expand=d_expand)
|
711
|
+
|
712
|
+
return regions
|
713
|
+
|
714
|
+
def default_processing(self):
|
715
|
+
"""Default processing.
|
716
|
+
|
717
|
+
Performs several processing methods with attributes chosen
|
718
|
+
experimentally to achieve best results for the purpose of "fast",
|
719
|
+
"reliable" and "reproducible" NMR analysis.
|
720
|
+
"""
|
721
|
+
# TODO add processing for various nucleus
|
722
|
+
if self.parameters["rxChannel"] == "19F":
|
723
|
+
self.apodization(function="gm", g1=1.2, g2=4.5)
|
724
|
+
self.zero_fill()
|
725
|
+
self.fft()
|
726
|
+
self.correct_baseline()
|
727
|
+
self.autophase()
|
728
|
+
self.correct_baseline()
|
729
|
+
|
730
|
+
def integrate_area(self, area, rule="trapz"):
|
731
|
+
"""Integrate the spectrum within given area.
|
732
|
+
|
733
|
+
Redefined from ancestor method to discard imaginary part of the
|
734
|
+
resulting integral.
|
735
|
+
|
736
|
+
Args:
|
737
|
+
area (Tuple[float, float]): Tuple with left and right border (X axis
|
738
|
+
obviously) for the desired area.
|
739
|
+
rule (str, optional): Method for integration, "trapz" - trapezoidal
|
740
|
+
rule (default), "simps" - Simpson's rule.
|
741
|
+
Returns:
|
742
|
+
float: Definite integral within given area as approximated by given
|
743
|
+
method.
|
744
|
+
"""
|
745
|
+
|
746
|
+
result = super().integrate_area(area, rule)
|
747
|
+
|
748
|
+
# discarding imaginary part and returning the absolute value
|
749
|
+
# due to "NMR-order" of the x axis
|
750
|
+
return abs(result.real)
|
751
|
+
|
752
|
+
def integrate_regions(self, regions):
|
753
|
+
"""Integrate the given regions using nmrglue integration method.
|
754
|
+
|
755
|
+
Check the corresponding documentation for details.
|
756
|
+
|
757
|
+
Args:
|
758
|
+
regions (:obj:np.array): 2D Mx2 array, containing left and right
|
759
|
+
borders for the regions of interest, potentially containing
|
760
|
+
peak areas (as found by self.generate_peak_regions method).
|
761
|
+
|
762
|
+
Return:
|
763
|
+
result (:obj:np.array): 1D M-size array contaning integration for
|
764
|
+
each region of interest.
|
765
|
+
"""
|
766
|
+
|
767
|
+
result = ng.analysis.integration.integrate(
|
768
|
+
data=self.y,
|
769
|
+
unit_conv=self._uc,
|
770
|
+
limits=self.x[regions], # directly get the ppm values
|
771
|
+
)
|
772
|
+
|
773
|
+
# discarding imaginary part
|
774
|
+
return np.abs(np.real(result))
|
775
|
+
|
776
|
+
def reference_spectrum(
|
777
|
+
self,
|
778
|
+
new_position: float,
|
779
|
+
reference: Union[float, str] = "highest",
|
780
|
+
) -> None:
|
781
|
+
"""Shifts the spectrum x axis according to the new reference.
|
782
|
+
|
783
|
+
If old reference is omitted will shift the spectrum according to the
|
784
|
+
highest peak.
|
785
|
+
|
786
|
+
Args:
|
787
|
+
new_position (float): The position to shift the peak to.
|
788
|
+
reference (Union[float, str]): The current position of the reference
|
789
|
+
peak or it's indication for shifting: either "highest" (default)
|
790
|
+
or "closest" for selecting highest or closest to the new
|
791
|
+
reference peak for shifting.
|
792
|
+
"""
|
793
|
+
|
794
|
+
# find reference if not given
|
795
|
+
if isinstance(reference, str):
|
796
|
+
if reference == "highest":
|
797
|
+
# Looking for highest point
|
798
|
+
reference = self.x[np.argmax(self.y)]
|
799
|
+
elif reference == "closest":
|
800
|
+
# Looking for closest peak among found across whole spectrum
|
801
|
+
# Specifying area not to update self.peaks
|
802
|
+
peaks = self.find_peaks(area=(self.x.min(), self.x.max()))
|
803
|
+
# x coordinate
|
804
|
+
peaks_xs = peaks[:, 1].real
|
805
|
+
reference = peaks[np.argmin(np.abs(peaks_xs - new_position))][1].real
|
806
|
+
else:
|
807
|
+
self.logger.warning(
|
808
|
+
'Please use either "highest" or "closest"\
|
809
|
+
reference, or give exact value.'
|
810
|
+
)
|
811
|
+
return
|
812
|
+
|
813
|
+
new_position, _ = find_nearest_value_index(self.x, new_position)
|
814
|
+
|
815
|
+
diff = new_position - reference
|
816
|
+
|
817
|
+
# shifting the axis
|
818
|
+
self.x = self.x + diff
|
819
|
+
|
820
|
+
# if peaks are recorded, find new
|
821
|
+
if self.peaks is not None:
|
822
|
+
self.find_peaks()
|