/
spinsolve.py
306 lines (249 loc) · 10 KB
/
spinsolve.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
"""
Functions for reading Magritek Spinsolve binary (dx/1d) files and
parameter (acqu.par/proc.par) files.
"""
from warnings import warn
import os
import numpy as np
from . import fileiobase
from . import jcampdx
__developer_info__ = """
Spinsolve is the software used on the Magritek benchtop NMR devices.
A spectrum is saved in a folder with several files. The spectral data is
stored in these files: 'data.1d' (FID), 'spectrum.1d' (Fourier transformed)
and 'spectrum_processed.1d' (FT + processed by spinsolve)
Optional spectral data (System->Prefs->Setup->Global data storage):
'nmr_fid.dx' (FID stored in `JCAMP-DX standard <http://www.jcamp-dx.org/>`),
'spectrum.csv' and 'spectrum_processed.csv' (FT + processed by Spinsolve with ppm for each
point and intensity delimited by ';')
Other files:
'acqu.par' - all parameters that are used for acquisition
'protocol.par' - text file used to reload data back into the Spinsolve software
'processing.script' - text file to transfer Spinsolve software protocol settings
into MNOVA
The Spinsolve Expert software has a slightly different output:
[Needs to be double checked as I do not have access to this software -LCageman]
- Output into JCAMP-DX is not possible
- 'spectrum_processed.1d' is not generated
- (new) 'fid.1d' - seems to be the same as 'data.1d'
- (new) 'proc.par' - contains processing parameters in the same style as 'acqu.par'
- (new) .pt1 files - seem to be plot files specific for the expert software, cannot
be read by NMRglue
"""
def parse_spinsolve_par_line(line):
"""
Parse lines in acqu.par and return a tuple (parameter name, parameter value)
"""
line = line.strip() # Drop newline
name, value = line.split("=", maxsplit=1) # Split at equal sign (and ignore further equals in attribute values)
# remove spaces
name = name.strip()
value = value.strip()
# Detect value type
if value[0] == value[-1] == '"': # When flanked with " always go for string
return name, str(value[1:-1]) # Drop quote marks
else:
try:
return name, int(value) # ValueError when value == str(1.1), but not when value == str(1)
except ValueError:
try:
return name, float(value)
except ValueError:
return name, str(value)
def read(dir='.', specfile=None, acqupar="acqu.par", procpar="proc.par"):
"""
Reads spinsolve files from a directory
When no spectrum filename is given (specfile), the following list is tried, in
that specific order
["nmr_fid.dx", "data.1d", "fid.1d", "spectrum.1d", "spectrum_processed.1d"]
To use the resolution enhanced spectrum use the './Enhanced' folder as input.
Note that spectrum.1d and spectrum_processed.1d contain only data in the
frequency domain, so no Fourier transformation is needed. Also, use
dic["spectrum"]["xaxis"] to plot the x-axis
Parameters
----------
dir : str
Directory to read from
specfile : str, optional
Filename to import spectral data from. None uses standard filename from:
["nmr_fid.dx", "data.1d", "fid.1d", "spectrum.1d", "spectrum_processed.1d"]
acqupar : str, optional
Filename for acquisition parameters. None uses standard name.
procpar : str, optional
Filename for processing parameters. None uses standard name.
Returns
-------
dic : dict
All parameters that can be present in the data folder:
dic["spectrum"] - First bytes of spectrum(_processed).1d
dic["acqu"] - Parameters present in acqu.par
dic["proc"] - Parameters present in proc.par
dic["dx"] - - Parameters present in the header of nmr_fid.dx
data : ndarray
Array of NMR data
"""
if os.path.isdir(dir) is not True:
raise OSError("directory %s does not exist" % (dir))
# Create empty dic
dic = {
"spectrum": {},
"acqu": {},
"proc": {},
"dx": {}
}
# Read in acqu.par and write to dic
acqupar = os.path.join(dir, acqupar)
if os.path.isfile(acqupar):
with open(acqupar, "r") as f:
info = f.readlines()
for line in info:
par_name, par_value = parse_spinsolve_par_line(line)
if par_name is not None:
dic["acqu"][par_name] = par_value
# Read in proc.par and write to dic
procpar = os.path.join(dir, procpar)
if os.path.isfile(procpar):
with open(procpar) as f:
info = f.readlines()
for line in info:
line = line.replace("\n", "")
k, v = line.split("=")
dic["proc"][k.strip()] = v.strip()
# Define which spectrumfile to take, using 'specfile' when defined, otherwise
# the files in 'priority_list' are tried, in that particular order
priority_list = ["nmr_fid.dx", "data.1d", "fid.1d", "spectrum.1d", "spectrum_processed.1d"]
inputfile = None
if specfile:
inputfile = os.path.join(dir, specfile)
if not os.path.isfile(inputfile):
raise OSError("File %s does not exist" % (inputfile))
else:
for priority in priority_list:
inputfile = os.path.join(dir, priority)
if os.path.isfile(inputfile):
break
if inputfile is None:
raise OSError("directory %s does not contain spectral data" % (dir))
# Detect which file we are dealing with from the extension and read in the spectral data
# Reading .dx file using existing nmrglue.fileio.jcampdx module
if inputfile.split('.')[-1] == "dx":
dic["dx"], raw_data = jcampdx.read(inputfile)
data = raw_data[0][:] + 1j * raw_data[1][:]
# Reading .1d files
elif inputfile.split('.')[-1] == "1d":
with open(inputfile, "rb") as f:
raw_data = f.read()
# Write out parameters from the first 32 bytes into dic["spectrum"]
keys = ["owner", "format", "version", "dataType", "xDim", "yDim", "zDim", "qDim"]
for i, k in enumerate(keys):
start = i * 4
end = start + 4
value = int.from_bytes(raw_data[start:end], "little")
dic["spectrum"][k] = value
data = np.frombuffer(raw_data[32:], "<f")
# The first 1/3 of the file is xaxis data (s or ppm)
split = data.shape[-1] // 3
xscale = data[0: split]
dic["spectrum"]["xaxis"] = xscale
# The rest is real and imaginary data points interleaved
data = data[split:: 2] + 1j * data[split + 1:: 2]
else:
raise OSError("File %s cannot be interpreted, use .dx or .1d instead" % (inputfile))
return dic, data
def make_uc(dic, data):
"""
Create a unit conversion object (only 1D).
Essentially identical to fileiobase.uc_from_udic() but uses data.size for size
so that it still works after zero filling
Parameters
----------
dic : dict
Dictionary of Spinsolve parameters.
data : ndarray
Array of NMR data.
Returns
-------
uc : unit conversion object
Unit conversion object for given dimension.
"""
udic = guess_udic(dic, data)
return fileiobase.unit_conversion(data.size, udic[0]['complex'], udic[0]['sw'], udic[0]['obs'], udic[0]['car'])
def get_udic_from_acqu_dict(param: dict):
"""
Returns an udic from the parameters in acqu dictionary provided.
"""
return_dict = dict()
# Spectral Width (sw, in Hz)
return_dict['sw'] = float(param.get('bandwidth')) * 1000
# Observation Frequency (obs, in MHz)
return_dict['obs'] = float(param.get('b1Freq'))
# Carrier frequency (car, in Hz)
return_dict['car'] = float(param.get('lowestFrequency')) + (return_dict['sw'] / 2)
# Label (str describing the axis name)
return_dict['label'] = param['rxChannel']
return return_dict
def get_udic_from_jcamp_dict(param):
"""
Returns an udic from the parameters in the JCAMP-DX dict provided.
"""
return_dict = dict()
# Observation Frequency (obs, in MHz)
try:
return_dict['obs'] = float(param['$BF1'][0])
except KeyError:
warn("Cannot set observation frequency - set manually using: 'udic[0]['obs'] = x' "
"where x is magnetic field in MHz")
# Spectral Width (sw, in Hz)
try:
return_dict['sw'] = float(param['$SW'][0]) * return_dict['obs']
except KeyError:
warn("Cannot set spectral width - set manually using: 'udic[0]['sw'] = x' "
"where x is the spectral width in Hz")
# Carrier frequency (car, in Hz)
try:
return_dict['car'] = -float(param['$REFERENCEPOINT'][0]) + (return_dict['sw'] / 2)
except KeyError:
pass
try:
return_dict['car'] = (return_dict['obs'] - float(param['$SF'][0])) * 1e6
except KeyError:
warn("Cannot set carrier - try: 'udic[0]['car'] = x * udic[0]['obs']' "
"where x is the center of the spectrum in ppm")
# Label (str describing the axis name)
try:
return_dict['label'] = param[".OBSERVENUCLEUS"][0].replace("^", "")
except KeyError:
warn("Cannot set observed nucleus label")
return return_dict
def guess_udic(dic, data):
"""
Guess parameters of universal dictionary from dic, data pair.
Parameters
----------
dic : dict
Dictionary of JCAMP-DX, acqu, proc and spectrum parameters.
data : ndarray
Array of NMR data.
Returns
-------
udic : dict
Universal dictionary of spectral parameters.
"""
# Create an empty universal dictionary
udic = fileiobase.create_blank_udic(1)
# Size can be calculated from data len
if data is not None:
udic[0]["size"] = len(data)
else:
warn('No data, cannot set udic size')
# For the other parameters, first try to use info from acqu.par (dic['acqu']), then JCAMP-DX header parameters
if dic["acqu"]:
udic[0].update(get_udic_from_acqu_dict(dic["acqu"]))
else:
udic[0].update(get_udic_from_jcamp_dict(dic["dx"]))
# keys left to default
# udic[0]['complex']
# udic[0]['encoding']
# udic[0]['time'] = True
# udic[0]['freq'] = False
return udic