1
0
forked from IPKM/nmreval

Compare commits

..

4 Commits

Author SHA1 Message Date
47feebe22f use C function and integration for anisotropic diffusion 2025-05-02 11:35:01 +00:00
11fe47bef1 add sigmoid function to basic models (#307)
fixes #306

Co-authored-by: Dominik Demuth <dominik.demuth@physik.tu-darmstadt.de>
Reviewed-on: IPKM/nmreval#307
2025-02-11 17:31:00 +00:00
f33643955b 304-fitparameter (#305)
fixes #304

Co-authored-by: Dominik Demuth <dominik.demuth@physik.tu-darmstadt.de>
Reviewed-on: IPKM/nmreval#305
2025-01-23 18:11:45 +00:00
41353b9a54 wide-line spectra handle missing x values better (#303)
see issue #302

Co-authored-by: Dominik Demuth <dominik.demuth@physik.tu-darmstadt.de>
Reviewed-on: IPKM/nmreval#303
2024-12-09 13:45:07 +00:00
10 changed files with 168 additions and 59 deletions

View File

@ -133,7 +133,7 @@ class QAsciiReader(QtWidgets.QDialog, Ui_ascii_reader):
if self.column_checkBox.isChecked() and self.line_spinBox.isEnabled(): if self.column_checkBox.isChecked() and self.line_spinBox.isEnabled():
header_line = self.reader.header[self.line_spinBox.value()-1] header_line = self.reader.header[self.line_spinBox.value()-1]
header_line = header_line.strip('\n\t\r, ') header_line = header_line.strip('\n\t\r, ')
header_line = re.sub(r'[\t ;,]+', ';', header_line) header_line = re.sub(r'[\t, ;]+(?!\w*})', ';', header_line)
self.ascii_table.setHorizontalHeaderLabels(header_line.split(';')) self.ascii_table.setHorizontalHeaderLabels(header_line.split(';'))

View File

@ -542,7 +542,9 @@ class UpperManagement(QtCore.QObject):
elif fit_limits[0] == 'in': elif fit_limits[0] == 'in':
inside = np.where((_x >= fit_limits[1][0]) & (_x <= fit_limits[1][1])) inside = np.where((_x >= fit_limits[1][0]) & (_x <= fit_limits[1][1]))
else: else:
inside = np.where((_x < fit_limits[1][0]) | (_x > fit_limits[1][1])) x_lim, _ = self.graphs[self.current_graph].ranges
inside_graph = (_x >= x_lim[0]) & (_x <= x_lim[1])
inside = np.where(((_x < fit_limits[1][0]) | (_x > fit_limits[1][1])) & inside_graph)
try: try:
if isinstance(we, str): if isinstance(we, str):

View File

@ -0,0 +1,16 @@
/* integrands used in quadrature integration with scipy's LowLevelCallables */
#include <math.h>
double anistropicDiffusion(double x, void *user_data) {
double *c = (double *)user_data;
double q = c[0];
double t = c[1];
double d_perp = c[2];
double d_par = c[3];
double cos_theta = cos(x);
double sin_theta = sin(x);
return exp(-q * q * t * (d_par * cos_theta * cos_theta + d_perp * sin_theta * sin_theta)) * sin_theta;
}

BIN
src/nmreval/clib/diffusion.so Executable file

Binary file not shown.

View File

@ -5,6 +5,17 @@ from ctypes import CDLL, c_double, c_void_p
from ..lib.logger import logger from ..lib.logger import logger
diffusion_lib = None
try:
diffusion_lib = CDLL(str(Path(__file__).parents[1] / 'clib' / 'diffusion.so'))
diffusion_lib.anistropicDiffusion.restype = c_double
diffusion_lib.anistropicDiffusion.argtypes = (c_double, c_void_p)
HAS_C_FUNCS = True
except OSError:
HAS_C_FUNCS = False
lib = None lib = None
try: try:
lib = CDLL(str(Path(__file__).parents[1] / 'clib' / 'integrate.so')) lib = CDLL(str(Path(__file__).parents[1] / 'clib' / 'integrate.so'))
@ -39,10 +50,8 @@ try:
lib.energyDistSuscImag.restype = c_double lib.energyDistSuscImag.restype = c_double
lib.energyDistSuscImag.argtypes = (c_double, c_void_p) lib.energyDistSuscImag.argtypes = (c_double, c_void_p)
HAS_C_FUNCS = True HAS_C_FUNCS = True
logger.info('Use C functions') logger.info('Use C functions')
except OSError: except OSError:
HAS_C_FUNCS = False HAS_C_FUNCS = False
logger.info('Use python functions') logger.info('Use python functions')

View File

@ -49,7 +49,7 @@ class AsciiReader:
with self.fname.open('r') as f: with self.fname.open('r') as f:
for i, line in enumerate(islice(f, len(self.header)+len(self.lines), num_lines)): for i, line in enumerate(islice(f, len(self.header)+len(self.lines), num_lines)):
line = line.strip('\n\t\r, ') line = line.strip('\n\t\r, ')
line = re.sub(r'[\t ;,]+', ';', line) line = re.sub(r'[\t, ;]+(?!\w*})', ';', line)
line = line.split(';') line = line.split(';')
try: try:
@ -146,10 +146,11 @@ class AsciiReader:
raw_data = raw_data.reshape((1, *raw_data.shape)) raw_data = raw_data.reshape((1, *raw_data.shape))
if len(x) == 0 or raw_data.shape[2] == 1: if len(x) == 0 or raw_data.shape[2] == 1:
_temp = np.zeros((raw_data.shape[0], raw_data.shape[1], raw_data.shape[2]+1)) raw_data = raw_data.reshape(raw_data.shape[0], raw_data.shape[2], raw_data.shape[1])
_temp[:, :, 0] = np.arange(raw_data.shape[1]) # _temp = np.zeros((raw_data.shape[0], raw_data.shape[2], raw_data.shape[1]))
_temp[:, :, 1:] = raw_data # _temp[:, :, 0] = np.arange(raw_data.shape[1])
raw_data = _temp # _temp[:, :, 1:] = raw_data
# raw_data = _temp
if y: if y:
y = [i+1 for i in y] y = [i+1 for i in y]

View File

@ -203,6 +203,31 @@ class Sinc:
return c * np.sinc(((x-x0)/w)/np.pi) return c * np.sinc(((x-x0)/w)/np.pi)
class Sigmoid:
type = 'Basic'
name = 'Sigmoid'
equation = 'C / [1 + exp(-a * (x - x_{0})] + y_{0}'
params = ['C', 'a', 'x_{0}', 'y_{0}']
@staticmethod
def func(x, c, a, x0, y0):
"""
Sigmoid function
.. math::
y = C / [1 + exp(-a * (x - x_0))] + y_0
Args:
x (array_like): Input values
c (float): Prefactor
a (float): Steepness of the sigmoid
x0 (float): x position of the sigmoid's midpoint
y0 (float): y position of the sigmoid's midpoint
"""
return c / (1 + np.exp(-a * (x - x0))) + y0
class Sine: class Sine:
""" """
Wavy sine function Wavy sine function

View File

@ -1,7 +1,11 @@
from ctypes import c_double, cast, c_void_p, pointer
import numpy as np import numpy as np
from scipy import special as special from scipy import special as special, LowLevelCallable
from scipy.integrate import quad
from ..utils import gamma from ..utils import gamma
from nmreval.distributions.helper import HAS_C_FUNCS, diffusion_lib
class Diffusion: class Diffusion:
@ -103,20 +107,36 @@ class AnisotropicDiffusion(object):
tp = x tp = x
relax = np.exp(-(tp/trel)**brel)*np.exp(-(tp/trel)**brel) relax = np.exp(-(tp/trel)**brel)*np.exp(-(tp/trel)**brel)
q_squared = np.power(g * nucleus * tp, 2) q = g * nucleus * tp
t = 2 * tp / 3 + tm t = 2 * tp / 3 + tm
z = np.sqrt(q_squared * (d_par - d_perp) * t)
# Callaghan eq (6.89) # Callaghan eq (6.89)
diffs = np.exp(-q_squared*t*d_perp) * special.erf(z) / z if HAS_C_FUNCS:
# divide by 2 to normalize by integral sin(x), x=0..pi
diffusion_decay = AnisotropicDiffusion._integrate_c(q, t, d_perp, d_par) / 2
else:
z = np.sqrt(q**2 * (d_par - d_perp) * t)
diffusion_decay = np.exp(-q**2 * t * d_perp) * special.erf(z) / z
return m0 * diffs * relax return m0 * diffusion_decay * relax
@staticmethod
def _integrate_c(q, t, d_perp, d_par) -> np.ndarray:
diffusion_decay = np.zeros_like(t)
for (i, t_i) in enumerate(t):
c = (c_double * 4)(q, t_i, d_perp, d_par)
user_data = cast(pointer(c), c_void_p)
diffusion_decay[i] = quad(LowLevelCallable(diffusion_lib.anistropicDiffusion, user_data), 0, np.pi, epsabs=1e-13)[0]
return diffusion_decay
class Peschier: class Peschier:
name = 'Diffusion + Cross-Relaxation' name = 'Diffusion + Cross-Relaxation'
type = 'Diffusion' type = 'Diffusion'
equation = r'Diffusion with cross-relax f(ast) \rightarrow s(low)' equation = r'Diffusion with cross-relax f(ast) \rightarrow s(low)'
params = ['M_{0}', 'D', 'T_{1,f}', 'T_{1,s}', 'k', 'p_{f}', 't_{ev}', 'g'] params = ['M_{0}', 'D', 'T_{1f}', 'T_{1s}', 'k', 'p_{f}', 't_{ev}', 'g']
bounds = [(0, None), (0, None), (0, None), (0, None), (0, None), (0, None)] bounds = [(0, None), (0, None), (0, None), (0, None), (0, None), (0, None)]
choices = [(r'\gamma', 'nucleus', gamma)] choices = [(r'\gamma', 'nucleus', gamma)]

View File

@ -75,7 +75,7 @@ class TwoSatRecAbsolute:
type = 'Relaxation' type = 'Relaxation'
name = 'Two-step relaxation (abs. int)' name = 'Two-step relaxation (abs. int)'
equation = r'M_{0} + \Sigma \DeltaM_{i}(1-exp(-(x/T_{1,i})^{\beta_{i}}))' equation = r'M_{0} + \Sigma \DeltaM_{i}(1-exp(-(x/T_{1,i})^{\beta_{i}}))'
params = [r'\DeltaM_{1}', 'T_{1,1}', r'\beta_{1}', r'\DeltaM_{2}', 'T_{1,2}', r'\beta_{2}', 'M_{0}'] params = [r'\DeltaM_{1}', 'T_{11}', r'\beta_{1}', r'\DeltaM_{2}', 'T_{12}', r'\beta_{2}', 'M_{0}']
choices = [('Type', 'is_inv', {'Saturation': False, 'Inversion': True})] choices = [('Type', 'is_inv', {'Saturation': False, 'Inversion': True})]
bounds = [(0, None), (0, None), (0, 1), (0, None), (0, None), (0, 1), (None, None)] bounds = [(0, None), (0, None), (0, 1), (0, None), (0, None), (0, 1), (None, None)]
@ -92,7 +92,7 @@ class TwoSatRecRelative:
name = 'Two-step relaxation (rel. int)' name = 'Two-step relaxation (rel. int)'
equation = r'M_{0} + \DeltaM[R(1-exp(-(x/T_{1,1})^{\beta_{1}})) + \n'\ equation = r'M_{0} + \DeltaM[R(1-exp(-(x/T_{1,1})^{\beta_{1}})) + \n'\
r'(1-R)(1-exp(-(x/T_{1,2})^{\beta_{2}}))]' r'(1-R)(1-exp(-(x/T_{1,2})^{\beta_{2}}))]'
params = [r'\DeltaM', 'M_{0}', 'T_{1,1}', r'\beta_{1}', 'T_{1,2}', r'\beta_{2}', 'R'] params = [r'\DeltaM', 'M_{0}', 'T_{11}', r'\beta_{1}', 'T_{12}', r'\beta_{2}', 'R']
choices = [('Type', 'kind', {'Saturation': 'sat', 'Inversion': 'inv'})] choices = [('Type', 'kind', {'Saturation': 'sat', 'Inversion': 'inv'})]
bounds = [(0, None), (None, None), (0, None), (0, 1), (0, None), (0, 1), (0, 1)] bounds = [(0, None), (None, None), (0, None), (0, 1), (0, None), (0, 1), (0, 1)]

View File

@ -3,11 +3,42 @@ try:
from scipy.integrate import simpson from scipy.integrate import simpson
except ImportError: except ImportError:
from scipy.integrate import simps as simpson from scipy.integrate import simps as simpson
from numpy import pi
from ..math.orientations import zcw_spherical as crystallites from ..math.orientations import zcw_spherical as crystallites
__all__ = ['CSA', 'Pake', 'SecCentralLine']
def _make_broadening(x: np.ndarray, sigma: float, mode: str):
dx = x[1] - x[0]
_x = np.arange(len(x)) * dx
_x -= 0.5 * _x[-1]
if mode == 'l':
apd = 2 * sigma / (4*_x**2 + sigma**2) / np.pi
else:
ln2 = np.log(2)
apd = np.exp(-4*ln2 * (_x/sigma)**2) * 2 * np.sqrt(ln2/np.pi) / sigma
return apd
def _make_bins(x: np.ndarray) -> np.ndarray:
bins = 0.5 * (x[1:] + x[:-1])
return np.r_[0.5 * (-x[1] + 3 * x[0]), bins, 0.5 * (3 * x[-1] - x[-2])]
def _make_x(x: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
_x = x
dx = x[1:] - x[:-1]
dx = np.min(dx)
width = x[-1] - x[0]
_x = np.arange(width/dx - 1) * dx + x[0]
bins = (_x[1:] + _x[:-1]) / 2
bins = np.r_[_x[0]-dx/2, bins, _x[-1] + dx/2]
return _x, bins
class Pake: class Pake:
type = 'Spectrum' type = 'Spectrum'
name = 'Pake' name = 'Pake'
@ -17,38 +48,39 @@ class Pake:
choices = [('Broadening', 'broad', {'Gaussian': 'g', 'Lorentzian': 'l'})] choices = [('Broadening', 'broad', {'Gaussian': 'g', 'Lorentzian': 'l'})]
@staticmethod @staticmethod
def func(x, c, delta, eta, sigma, t_pulse, broad='g'): def func(
x: np.ndarray,
c: float,
delta: float,
eta: float,
sigma: float,
t_pulse: float,
broad: str = 'g',
) -> np.ndarray:
a, b, _ = crystallites(100000) a, b, _ = crystallites(100000)
bins = 0.5 * (x[1:] + x[:-1])
bins = np.r_[0.5*(3*x[0]-x[1]), bins, 0.5*(3*x[-1]-x[-2])]
omega = delta * 0.5 * (3*np.cos(b)**2 - 1 - eta * np.sin(b)**2 * np.cos(2*a)) omega = delta * 0.5 * (3*np.cos(b)**2 - 1 - eta * np.sin(b)**2 * np.cos(2*a))
x_used, bins = _make_x(x)
s_left = np.histogram(omega, bins=bins)[0] s_left = np.histogram(omega, bins=bins)[0]
s_right = np.histogram(-omega, bins=bins)[0] s_right = np.histogram(-omega, bins=bins)[0]
s = s_left + s_right s = s_left + s_right
if sigma != 0: if sigma != 0:
_x = np.arange(len(x))*(x[1]-x[0]) apd = _make_broadening(x_used, sigma, broad)
_x -= 0.5*_x[-1]
if broad == 'l':
apd = 2 * sigma / (4 * _x**2 + sigma**2) / pi
else:
apd = np.exp(-4 * np.log(2) * (_x/sigma)**2) * 2 * np.sqrt(np.log(2) / pi) / sigma
ret_val = np.convolve(s, apd, mode='same') ret_val = np.convolve(s, apd, mode='same')
else: else:
ret_val = s ret_val = s
omega_1 = pi/2/t_pulse omega_1 = np.pi/2/t_pulse
attn = omega_1 * np.sin(t_pulse*np.sqrt(omega_1**2+0.5*(2*pi*x)**2)) / \ attn = omega_1 * np.sin(t_pulse*np.sqrt(omega_1**2 + 0.5*(2*np.pi*x_used)**2)) / np.sqrt(omega_1**2 + (np.pi*x_used)**2)
np.sqrt(omega_1**2+(np.pi*x)**2)
ret_val *= attn ret_val *= attn
ret_val /= simpson(y=ret_val, x=x_used)
return c * ret_val / simpson(ret_val, x) if x_used.size == x.size:
return c * ret_val
else:
return c * np.interp(x=x, xp=x_used, fp=ret_val)
class CSA: class CSA:
@ -60,28 +92,29 @@ class CSA:
choices = [('Broadening', 'broad', {'Gaussian': 'g', 'Lorentzian': 'l'})] choices = [('Broadening', 'broad', {'Gaussian': 'g', 'Lorentzian': 'l'})]
@staticmethod @staticmethod
def func(x, c, delta, eta, w_iso, sigma, broad='g'): def func(
x: np.ndarray,
c: float,
delta: float,
eta: float,
w_iso: float,
sigma: float,
broad: str = 'g',
) -> np.ndarray:
a, b, _ = crystallites(100000) a, b, _ = crystallites(100000)
bins = 0.5 * (x[1:] + x[:-1])
bins = np.r_[0.5*(-x[1] + 3*x[0]), bins, 0.5*(3*x[-1] - x[-2])]
omega = w_iso + delta * 0.5 * (3*np.cos(b)**2 - 1 - eta * np.sin(b)**2 * np.cos(2*a)) omega = w_iso + delta * 0.5 * (3*np.cos(b)**2 - 1 - eta * np.sin(b)**2 * np.cos(2*a))
s_left = np.histogram(omega, bins=bins)[0] s = np.histogram(omega, bins=_make_bins(x))[0]
s = s_left
if sigma != 0: if sigma != 0:
_x = np.arange(len(x)) * (x[1] - x[0]) print(len(s))
_x -= 0.5 * _x[-1] apd = _make_broadening(x, sigma, broad)
if broad == 'l':
apd = 2 * sigma / (4*_x**2 + sigma**2) / pi
else:
apd = np.exp(-4 * np.log(2) * (_x / sigma) ** 2) * 2 * np.sqrt(np.log(2) / pi) / sigma
ret_val = np.convolve(s, apd, mode='same') ret_val = np.convolve(s, apd, mode='same')
else: else:
ret_val = s ret_val = s
return c * ret_val / simpson(ret_val, x) return c * ret_val / simpson(y=ret_val, x=x)
class SecCentralLine: class SecCentralLine:
@ -94,10 +127,18 @@ class SecCentralLine:
('Broadening', 'broad', {'Gaussian': 'g', 'Lorentzian': 'l'})] ('Broadening', 'broad', {'Gaussian': 'g', 'Lorentzian': 'l'})]
@staticmethod @staticmethod
def func(x, c, cq, eta, f_iso, gb, f_l, spin=2.5, broad='g'): def func(
x: np.ndarray,
c: float,
cq: float,
eta: float,
f_iso: float,
gb: float,
f_l: float,
spin: float = 2.5,
broad: str = 'g',
) -> np.ndarray:
a, b, _ = crystallites(200000) a, b, _ = crystallites(200000)
bins = 0.5 * (x[1:] + x[:-1])
bins = np.r_[0.5*(-x[1] + 3*x[0]), bins, 0.5*(3*x[-1] - x[-2])]
# coupling constant # coupling constant
omega_q = 2 * np.pi * cq / (2*spin*(2*spin-1)) omega_q = 2 * np.pi * cq / (2*spin*(2*spin-1))
@ -116,17 +157,12 @@ class SecCentralLine:
orient += prefactor_c orient += prefactor_c
omega = 2*np.pi*f_iso + coupling * orient omega = 2*np.pi*f_iso + coupling * orient
s = np.histogram(omega / (2*np.pi), bins=bins)[0] s = np.histogram(omega / (2*np.pi), bins=_make_bins(x))[0]
if gb != 0: if gb != 0:
_x = np.arange(len(x)) * (x[1]-x[0]) apd = _make_broadening(x, gb, broad)
_x -= 0.5*_x[-1]
if broad == 'l':
apd = 2*gb / (4*_x**2 + gb**2) / np.pi
else:
apd = np.exp(-4*np.log(2) * (_x/gb)**2) * 2 * np.sqrt(np.log(2)/np.pi) / gb
ret_val = np.convolve(s, apd, mode='same') ret_val = np.convolve(s, apd, mode='same')
else: else:
ret_val = s ret_val = s
return c * ret_val / simpson(ret_val, x) return c * ret_val / simpson(y=ret_val, x=x)