2022-03-22 20:07:59 +01:00

203 lines
5.6 KiB
Python

import warnings
from typing import Union
import numpy as np
from .signals import Signal
class FID(Signal):
"""
Extensions if :class:`Signal` for NMR timesignals
"""
def __init__(self, x, y, **kwargs):
super().__init__(x, y, **kwargs)
self.meta['apod'] = []
self.meta['zf'] = 0
def __repr__(self):
return f"FID: {self.name}"
def baseline(self, percentage: float = 0.12):
"""
Substract
Args:
percentage:
Returns:
"""
self._y -= self._y[int(-percentage * self.length):].mean()
return self
def apod(self, p, method=None):
try:
self._y *= method.apod(self._x, *p)
self.meta['apod'].append((str(method), p))
except KeyError:
print(f'Unknown apodization {method}')
def zerofill(self, pad: int = 1):
_max_x = self._x.max()
factor = 2**pad
if factor < 1:
self._x = self._x[:int(self.length*factor)]
self._y = self._y[:int(self.length*factor)]
self._y_err = self._y_err[:int(self.length*factor)]
self.mask = self.mask[:int(self.length*factor)]
elif factor > 1:
add_length = int((factor-1) * self.length)
self._y = np.concatenate((self._y, np.zeros(add_length)))
self._y_err = np.concatenate((self._y_err, np.zeros(add_length)))
self.mask = np.concatenate((self.mask, np.ones(add_length, dtype=bool)))
_temp_x = np.arange(1, add_length+1) * self.dx + np.max(self._x)
self._x = np.concatenate((self._x, _temp_x))
self.meta['zf'] += pad
return self
def fourier(self) -> 'Spectrum':
ft = np.fft.fftshift(np.fft.fft(self._y)) / self.dx
freq = np.fft.fftshift(np.fft.fftfreq(self.length, self.dx))
spec = Spectrum(freq, ft, dx=self.dx)
spec.update(self.meta)
return spec
def fft_depake(self, scale: bool = False):
"""
Calculate deconvoluted Pake spectra
Args:
scale (bool):
Reference:
M.A. McCabe, S.R. Wassail:
Rapid deconvolution of NMR powder spectra by weighted fast Fourier transformation,
SSNMR (1997), Vol.10, Nr.1-2, pp. 53-61
"""
_y = self._y + 0
# Perform FFT depaking
_y *= np.sqrt(self._x)
# built halves
right = np.fft.fftshift(np.fft.fft(_y)) * (1 + 1j)
left = np.fft.fftshift(np.fft.fft(_y)) * (1 - 1j)
freq = np.fft.fftshift(np.fft.fftfreq(self.length, self.dx))
if scale:
# fourier transform should be multiplied with \sqrt(|omega|)
# but lines at omega=0 are suppressed, so sometimes not a good idea
right *= np.sqrt(np.abs(freq))
left *= np.sqrt(np.abs(freq))
# built depaked spectrum
new_spc = np.concatenate([left[:(self.length//2)], right[(self.length//2):]])
spec = Spectrum(2*freq, new_spc, dx=self.dx)
spec.update(self.meta)
return spec
def convert(self):
spec = Spectrum(self._x, self._y, dx=self.dx)
self.update(self.meta)
return spec
def _calc_noise(self):
# noise per channel, so divide by sqrt(2)
self._y_err = np.std(self._y[-int(0.1*self.length):])/1.41
return self._y_err * np.ones(self.length)
def shift(self, points: int, mode: str = 'pts') -> None:
"""
Shift y values to new x indexes
Args:
points : shift value,
mode (str):
Returns:
"""
if mode == 'pts':
super().shift(int(points))
else:
pts = int(points//self.dx)
super().shift(pts)
class Spectrum(Signal):
"""
Extension of :class:`Signal` for NMR spectra
"""
def __init__(self, x, y, dx=None, **kwargs):
super().__init__(x, y, **kwargs)
if dx is None:
if 2 * abs(self._x[0])/len(self._x) - abs(self._x[1]-self._x[0]) < 1e-9:
self.dx = 1/(2 * abs(self._x[0]))
else:
self.dx = 1
else:
self.dx = dx
def __repr__(self):
return f"Spectrum: {self.name}"
def baseline(self, percentage: float = 0.12):
corr = np.mean([self._y[int(-percentage*self.length):], self._y[:int(percentage*self.length)]])
self._y -= corr
return self
def fourier(self) -> FID:
ft = np.fft.ifft(np.fft.ifftshift(self._y * self.dx))
t = np.arange(len(ft))*self.dx
shifted = np.fft.ifftshift(self._x)[0]
if shifted != 0:
warnings.warn(f'Shift of {shifted}: Spectrum is not zero-centered and FFT may give wrong results!')
ft *= np.exp(1j * shifted * 2*np.pi*t)
fid = FID(t, ft)
fid.update(self.meta)
return fid
def convert(self):
fid = FID(self._x, self._y)
fid.update(self.meta)
return fid
def _calc_noise(self):
step = int(0.05 * self.length)
start = np.argmin(abs(self._x - max(self._x.min(), -2e5)))
stop = np.argmin(abs(self._x - min(self._x.max(), 2e5)))
if start > stop:
stop, start = start, stop
stop -= step
self._y_err = np.inf
for i in range(start, stop):
# scan regions to find minimum noise (undisturbed by signals)
self._y_err = min(np.std(self._y[i:i + step])/np.sqrt(2), self._y_err)
return self._y_err * np.ones(self.length)
def shift(self, points: int, mode: str = 'pts'):
if mode == 'pts':
super().shift(int(points))
else:
return self