Lorentzian + Isotropic Rotational diffusion ∗ Resolution with bumps
Introduction
The objective of this notebook is to show how to use a combination of models from the QENS library i.e. Lorentzian and IsotropicRotationalDiffusion models.
The data are a set of water data measured at IN5 (ILL).
Reference: J. Qvist, H. Schober and B. Halle, J. Chem. Phys. 134, 144508 (2011)
Physical units
For information about unit conversion, please refer to the jupyter notebook called Convert_units.ipynb in the tools folder.
Import libraries
[1]:
import ipywidgets
import h5py
import QENSmodels
import numpy as np
from scipy.integrate import simps
import bumps.names as bmp
from bumps import fitters
from bumps.formatnum import format_uncertainty_pm
import matplotlib.pyplot as plt
%matplotlib widget
Setting of fitting
Load reference data
[2]:
path_to_data = './data/'
# Data
# Wavelength 5 Angstrom
with h5py.File(path_to_data + 'H2O_293K_5A.hdf', 'r') as f:
hw_5A = f['entry1']['data1']['X'][:]
q_5A = f['entry1']['data1']['Y'][:]
unit_w5A = f['entry1']['data1']['X'].attrs['long_name']
unit_q5A = f['entry1']['data1']['Y'].attrs['long_name']
sqw_5A = np.transpose(f['entry1']['data1']['DATA'][:])
err_5A = np.transpose(f['entry1']['data1']['errors'][:])
# Resolution
# Wavelength 5 Angstrom
with h5py.File(path_to_data + 'V_273K_5A.hdf', 'r') as f:
res_5A = np.transpose(f['entry1']['data1']['DATA'][:])
# Force resolution function to have unit area
# Wavelength 5 Angstrom
for i in range(len(q_5A)):
area = simps(res_5A[:, i], hw_5A)
res_5A[:, i] /= area
[3]:
fig, ax = plt.subplots(nrows=2, sharex=True)
for i in range(len(q_5A)):
ax[0].semilogy(hw_5A, sqw_5A[:,i], label=f"q={q_5A[i]:.1f}")
ax[1].semilogy(hw_5A, res_5A[:,i], label=f"q={q_5A[i]:.1f}")
ax[0].set_title(r'Signal 5 $\AA$')
ax[0].grid()
ax[1].set_title(r'Resolution 5 $\AA$')
ax[1].set_xlabel(f"$\hbar \omega$")
ax[1].grid()
Display units of input data
Just for information in order to determine if a conversion of units is required before using the QENSmodels
[4]:
print(f"At 5 Angstroms, the names and units of `w` (`x`axis) and `q` are: {unit_w5A[0].decode()} and {unit_q5A[0].decode()}, respectively.")
At 5 Angstroms, the names and units of `w` (`x`axis) and `q` are: Energy Transfer (meV) and Wavevector Transfer (A!U-1!N), respectively.
Create fitting model
[5]:
# Fit range -1 to +1 meV
idx_5A = np.where(np.logical_and(hw_5A > -1.0, hw_5A < 1.0))
# Fitting model
def model_convol(x, q, scale=1, center=0, hwhm=1, radius=1, DR=1, resolution=None):
model = QENSmodels.lorentzian(
x,
scale,
center,
hwhm
) + QENSmodels.sqwIsotropicRotationalDiffusion(
x,
q,
scale,
center,
radius,
DR
)
return np.convolve(model, resolution/resolution.sum(), mode='same')
# Fit
model_all_qs = []
for i in range(len(q_5A)):
x = hw_5A[idx_5A]
data = sqw_5A[idx_5A, i]
error = err_5A[idx_5A, i]
resol = res_5A[idx_5A, i]
# Select only valid data (error = -1 for Q, w points not accessible)
valid = np.where(error > 0.0)
x = x[valid[1]]
data = data[valid]
error = error[valid]
resol = resol[valid]
# model
model_q = bmp.Curve(
model_convol,
x, data, error,
name=f'q5A_{q_5A[i]:.2f}',
q=q_5A[i],
scale=15,
center=0.0,
hwhm=0.1,
radius=1.1,
DR=1.,
resolution=resol
)
# Fitted parameters
model_q.scale.range(0, 1e2)
model_q.center.range(-0.1, 0.1)
model_q.hwhm.range(0., 1)
model_q.radius.range(0.9, 1.1)
model_q.DR.range(0.01, 5)
# Q-independent parameters
if i == 0:
hwhm_q = model_q.hwhm
R_q = model_q.radius
DR_q = model_q.DR
else:
model_q.hwhm = hwhm_q
model_q.radius = R_q
model_q.DR = DR_q
model_all_qs.append(model_q)
problem = bmp.FitProblem(model_all_qs)
Display initial configuration: experimental data, fitting model with initial guesses
[6]:
slider = ipywidgets.IntSlider(value=0, min=0, max=len(q_5A)-1, continuous_update=False)
output = ipywidgets.Output()
def fig_q(model, ax, q_index=0):
"""
Plot of experimental data, fitting model and residual for a selected q value
Parameters
----------
model: list of bumps.curve.Curves for all q
ax: matplotlib.axes to be updated when changing the ipywidgets
q_index: int
index of q to be plotted
"""
model = model[q_index]
ax[0].errorbar(model.x,
model.y,
yerr=model.dy,
label='experimental data',
color='C0')
ax[0].plot(model.x,
model.theory(),
label='theory (model)',
color='C1')
ax[0].set_title(f'Model {model.name} - $\chi^2$={problem.chisq_str()}')
ax[0].legend()
ax[1].plot(model.x, model.residuals(), marker='o', linewidth=0, markersize=3, color='C0')
with output:
fig, ax = plt.subplots(nrows=2, ncols=1, sharex=True)
ax[0].grid(); ax[1].grid()
ax[1].set_ylabel('Residual')
ax[1].set_xlabel(f"$\hbar \omega$")
fig_q(model_all_qs, ax, 0)
def update_profile(change):
"""
Update plots for a new q-value
"""
with output:
ax[0].clear(); ax[1].lines.clear()
ax[0].grid()
fig_q(model_all_qs, ax,change['new'])
slider.observe(update_profile, names="value")
slider_label = ipywidgets.Label("q value to display")
slider_comp = ipywidgets.HBox([slider_label, slider])
ipywidgets.VBox([slider_comp, output])
[6]:
[7]:
problem.summarize().splitlines()
[7]:
[' DR .|........ 1 in (0.01,5)',
' center ....|..... 0 in (-0.1,0.1)',
' hwhm |......... 0.1 in (0,1)',
' radius .........| 1.1 in (0.9,1.1)',
' scale .|........ 15 in (0,100)',
' center ....|..... 0 in (-0.1,0.1)',
' scale .|........ 15 in (0,100)',
' center ....|..... 0 in (-0.1,0.1)',
' scale .|........ 15 in (0,100)',
' center ....|..... 0 in (-0.1,0.1)',
' scale .|........ 15 in (0,100)',
' center ....|..... 0 in (-0.1,0.1)',
' scale .|........ 15 in (0,100)',
' center ....|..... 0 in (-0.1,0.1)',
' scale .|........ 15 in (0,100)',
' center ....|..... 0 in (-0.1,0.1)',
' scale .|........ 15 in (0,100)',
' center ....|..... 0 in (-0.1,0.1)',
' scale .|........ 15 in (0,100)',
' center ....|..... 0 in (-0.1,0.1)',
' scale .|........ 15 in (0,100)',
' center ....|..... 0 in (-0.1,0.1)',
' scale .|........ 15 in (0,100)',
' center ....|..... 0 in (-0.1,0.1)',
' scale .|........ 15 in (0,100)',
' center ....|..... 0 in (-0.1,0.1)',
' scale .|........ 15 in (0,100)',
' center ....|..... 0 in (-0.1,0.1)',
' scale .|........ 15 in (0,100)',
' center ....|..... 0 in (-0.1,0.1)',
' scale .|........ 15 in (0,100)',
' center ....|..... 0 in (-0.1,0.1)',
' scale .|........ 15 in (0,100)',
' center ....|..... 0 in (-0.1,0.1)',
' scale .|........ 15 in (0,100)',
' center ....|..... 0 in (-0.1,0.1)',
' scale .|........ 15 in (0,100)']
Choice of minimizer for bumps
[8]:
options_dict = {}
for item in fitters.__dict__.keys():
if item.endswith('Fit') and fitters.__dict__[item].id in fitters.FIT_AVAILABLE_IDS:
options_dict[fitters.__dict__[item].name] = fitters.__dict__[item].id
w_choice_minimizer = ipywidgets.Dropdown(
options=list(options_dict.keys()),
value='Levenberg-Marquardt',
description='Minimizer:',
layout=ipywidgets.Layout(height='40px'))
w_choice_minimizer
[8]:
Number of steps for running fit using bumps
[9]:
steps_fitting = ipywidgets.IntText(
value=100,
step=100,
description='Number of steps when fitting',
style={'description_width': 'initial'})
steps_fitting
[9]:
Running the fit
Run the fit using the minimizer defined above with a number of steps also specified above.
[10]:
# Preview of the settings
print('Initial chisq', problem.chisq_str())
Initial chisq 43394.548(31)
[11]:
def settings_selected_optimizer(chosen_minimizer):
"""
List the settings available for the selected optimizer
This list can be used as arguments for the `fit` function
"""
assert type(chosen_minimizer) == ipywidgets.widgets.widget_selection.Dropdown
for item in fitters.__dict__.keys():
if item.endswith('Fit') and \
fitters.__dict__[item].id == options_dict[chosen_minimizer.value]:
return [elt[0] for elt in fitters.__dict__[item].settings]
[12]:
print((f"With {w_choice_minimizer.value} optimizer, "
f"you can use {settings_selected_optimizer(w_choice_minimizer)} as arguments of `fit`"))
With Levenberg-Marquardt optimizer, you can use ['steps', 'ftol', 'xtol'] as arguments of `fit`
[13]:
result = fitters.fit(
problem,
starts=10,
keep_best=True,
method=options_dict[w_choice_minimizer.value],
steps=int(steps_fitting.value)
)
Showing the results
[14]:
problem.summarize().splitlines()
[14]:
[' DR |......... 0.218667 in (0.01,5)',
' center ........|. 0.0718784 in (-0.1,0.1)',
' hwhm ..|....... 0.273972 in (0,1)',
' radius .........| 1.09993 in (0.9,1.1)',
' scale |......... 9.21634 in (0,100)',
' center .....|.... 0.00909587 in (-0.1,0.1)',
' scale |......... 9.46294 in (0,100)',
' center .....|.... 0.00807075 in (-0.1,0.1)',
' scale |......... 9.50003 in (0,100)',
' center .....|.... 0.00931405 in (-0.1,0.1)',
' scale |......... 9.49528 in (0,100)',
' center .....|.... 0.00547334 in (-0.1,0.1)',
' scale |......... 9.35253 in (0,100)',
' center .....|.... 0.00482186 in (-0.1,0.1)',
' scale |......... 8.78055 in (0,100)',
' center .....|.... 0.0048619 in (-0.1,0.1)',
' scale |......... 8.53799 in (0,100)',
' center .....|.... 0.000460866 in (-0.1,0.1)',
' scale |......... 8.14289 in (0,100)',
' center ....|..... -0.00151861 in (-0.1,0.1)',
' scale |......... 7.88231 in (0,100)',
' center .....|.... 0.0125892 in (-0.1,0.1)',
' scale |......... 8.04259 in (0,100)',
' center .....|.... 0.017195 in (-0.1,0.1)',
' scale |......... 7.99093 in (0,100)',
' center .....|.... 0.0148323 in (-0.1,0.1)',
' scale |......... 8.0245 in (0,100)',
' center .....|.... 0.0193758 in (-0.1,0.1)',
' scale |......... 7.59267 in (0,100)',
' center ......|... 0.0332099 in (-0.1,0.1)',
' scale |......... 7.06574 in (0,100)',
' center ......|... 0.0394203 in (-0.1,0.1)',
' scale |......... 7.01251 in (0,100)',
' center .......|.. 0.0516096 in (-0.1,0.1)',
' scale |......... 6.78668 in (0,100)',
' center .......|.. 0.0589272 in (-0.1,0.1)',
' scale |......... 6.56227 in (0,100)']
[15]:
# Other method to display the results of the fit (chi**2 and parameters' values)
print("final chisq", problem.chisq_str())
for k, v, dv in zip(problem.labels(), result.x, result.dx):
print(k, ":", format_uncertainty_pm(v, dv))
final chisq 3812.959(31)
DR : 0.21867 +/- 0.00080
center : 0.07188 +/- 0.00032
hwhm : 0.27397 +/- 0.00033
radius : 1.09993 +/- 0.00036
scale : 9.2163 +/- 0.0068
center : 0.00910 +/- 0.00032
scale : 9.4629 +/- 0.0066
center : 0.00807 +/- 0.00035
scale : 9.5000 +/- 0.0066
center : 0.00931 +/- 0.00037
scale : 9.4953 +/- 0.0065
center : 0.00547 +/- 0.00038
scale : 9.3525 +/- 0.0065
center : 0.00482 +/- 0.00041
scale : 8.7805 +/- 0.0064
center : 0.00486 +/- 0.00042
scale : 8.5380 +/- 0.0063
center : 0.46e-3 +/- 0.37e-3
scale : 8.1429 +/- 0.0062
center : -0.00152 +/- 0.00046
scale : 7.8823 +/- 0.0062
center : 0.01259 +/- 0.00041
scale : 8.0426 +/- 0.0064
center : 0.01719 +/- 0.00040
scale : 7.9909 +/- 0.0065
center : 0.01483 +/- 0.00038
scale : 8.0245 +/- 0.0065
center : 0.01938 +/- 0.00038
scale : 7.5927 +/- 0.0063
center : 0.03321 +/- 0.00038
scale : 7.0657 +/- 0.0062
center : 0.03942 +/- 0.00036
scale : 7.0125 +/- 0.0061
center : 0.05161 +/- 0.00036
scale : 6.7867 +/- 0.0061
center : 0.05893 +/- 0.00035
scale : 6.5623 +/- 0.0059
Display final configuration: experimental data, fitting model with output of fitting for the refined parameters
[16]:
slider1 = ipywidgets.IntSlider(value=0, min=0, max=len(q_5A)-1, continuous_update=False)
output1 = ipywidgets.Output()
with output1:
fig1, ax1 = plt.subplots(nrows=2, ncols=1, sharex=True)
ax1[0].grid(); ax1[1].grid()
ax1[1].set_ylabel('Residual')
ax1[1].set_xlabel(f"$\hbar \omega$")
fig_q(model_all_qs, ax1, 0)
def update_profile1(change):
"""
Update plots for a new q-value
"""
with output1:
ax1[0].clear(); ax1[1].lines.clear()
ax1[0].grid()
fig_q(model_all_qs, ax1, change['new'])
slider1.observe(update_profile1, names="value")
slider1_label = ipywidgets.Label("q value to display")
slider1_comp = ipywidgets.HBox([slider1_label, slider1])
ipywidgets.VBox([slider1_comp, output1])
[16]:
[ ]: