Isotropic Rotational Diffusion ∗ Resolution with bumps

Introduction

The objective of this notebook is to show how to use the isotropic rotational diffusion model IsotropicRotationalDiffusion to perform some fits using bumps .

The reference data were generated using the above model with the following parameters:
- Radius = 1.10 Å
- \(D_{rot}\) = 0.125 meV

The model was convoluted with a Gaussian resolution function of FWHM = 0.1 meV, centered randomly in the range [-0.01, +0.01] meV.

Finally the data were sampled randomly from a Poisson distribution.

The data do not have a background.

Physical units

For information about unit conversion, please refer to the jupyter notebook called Convert_units.ipynb in the tools folder.

The dictionary of units defined in the cell below specify the units of the refined parameters adapted to the convention used in the experimental datafile.

[1]:
# Units of parameters for selected QENS model and experimental data
dict_physical_units = {'omega': "meV",
                       'q': "1/Angstrom",
                       'DR': "meV",
                       'radius': "Angstrom",
                       'scale': "unit_of_signal.meV",
                       'center': "meV"}

Import libraries

[2]:
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

[3]:
path_to_data = './data/'

# Read the sample
with h5py.File(path_to_data + 'IsoRot_Sample.hdf', 'r') as f:
    hw = f['entry1']['data1']['X'][:]
    q = f['entry1']['data1']['Y'][:]
    unit_w = f['entry1']['data1']['X'].attrs['long_name']
    unit_q = f['entry1']['data1']['Y'].attrs['long_name']
    sqw = np.transpose(f['entry1']['data1']['DATA'][:])
    err = np.transpose(f['entry1']['data1']['errors'][:])

# Read resolution
with h5py.File(path_to_data + 'IsoRot_Resol.hdf', 'r') as f:
    res = np.transpose(f['entry1']['data1']['DATA'][:])

# Force resolution function to have unit area
for i in range(len(q)):
    area = simps(res[:,i], hw)
    res[:,i] /= area
[4]:
fig, ax = plt.subplots(nrows=2, sharex=True)

for i in range(len(q)):
    ax[0].semilogy(hw, sqw[:,i], label=f"q={q[i]:.1f}")
    ax[1].semilogy(hw, res[:,i], label=f"q={q[i]:.1f}")

ax[1].legend(bbox_to_anchor=(1.1, 1.3), loc='upper right', shadow=True)
ax[0].set_title('Signal')
ax[0].grid()

ax[1].set_title('Resolution')
ax[1].set_xlabel(f"$\hbar \omega ({dict_physical_units['center']})$")
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

[5]:
print(f"The names and units of `w` (`x`axis) and `q` are: {unit_w[0].decode()} and {unit_q[0].decode()}, respectively.")
The names and units of `w` (`x`axis) and `q` are: X and Y, respectively.

Create fitting model

[6]:
def model_convol(x, q, scale=1, center=0, radius=1, DR=1, resolution=None):
    model = QENSmodels.sqwIsotropicRotationalDiffusion(x, q, scale, center, radius, DR)
    return np.convolve(model, resolution/resolution.sum(), mode='same')
[7]:
# Fit
model_all_qs = []

# First dataset: wavelength=5 Angstrom
for i in range(len(q)):

    # Bumps fitting model
    model_q = bmp.Curve(
        model_convol,
        hw,
        sqw[:,i],
        err[:,i],
        name=f'q_{q[i]:.2f}',
        q=q[i],
        scale=1000,
        center=0.0,
        radius=1.0,
        DR=0.1,
        resolution=res[:, i]
    )
    model_q.scale.range(0, 1e5)
    model_q.center.range(-0.1, 0.1)
    model_q.radius.range(0, 3)
    model_q.DR.range(1e-12, 2)

    # Q-independent parameters
    if i == 0:
        R_q = model_q.radius
        DR_q = model_q.DR
    else:
        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

[8]:
slider = ipywidgets.IntSlider(value=0, min=0, max=len(q)-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 ({dict_physical_units['center']})$")
    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])
[8]:
[9]:
problem.summarize().splitlines()
[9]:
['                                      DR |.........        0.1 in (1e-12,2)',
 '                                  center ....|.....          0 in (-0.1,0.1)',
 '                                  radius ...|......          1 in (0,3)',
 '                                   scale |.........       1000 in (0,100000)',
 '                                  center ....|.....          0 in (-0.1,0.1)',
 '                                   scale |.........       1000 in (0,100000)',
 '                                  center ....|.....          0 in (-0.1,0.1)',
 '                                   scale |.........       1000 in (0,100000)',
 '                                  center ....|.....          0 in (-0.1,0.1)',
 '                                   scale |.........       1000 in (0,100000)',
 '                                  center ....|.....          0 in (-0.1,0.1)',
 '                                   scale |.........       1000 in (0,100000)',
 '                                  center ....|.....          0 in (-0.1,0.1)',
 '                                   scale |.........       1000 in (0,100000)',
 '                                  center ....|.....          0 in (-0.1,0.1)',
 '                                   scale |.........       1000 in (0,100000)',
 '                                  center ....|.....          0 in (-0.1,0.1)',
 '                                   scale |.........       1000 in (0,100000)',
 '                                  center ....|.....          0 in (-0.1,0.1)',
 '                                   scale |.........       1000 in (0,100000)',
 '                                  center ....|.....          0 in (-0.1,0.1)',
 '                                   scale |.........       1000 in (0,100000)']

Choice of minimizer for bumps

[10]:
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
[10]:

Setting for running bumps

[11]:
steps_fitting = ipywidgets.IntText(
    value=100,
    step=100,
    description='Number of steps when fitting',
    style={'description_width': 'initial'})

steps_fitting
[11]:
[12]:
# Preview of the settings
print('Initial chisq', problem.chisq_str())
Initial chisq 1067.0559(62)

Running the fit

Run the fit using the minimizer defined above with a number of steps also specified above.

[13]:
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]
[14]:
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`
[15]:
result = fitters.fit(
    problem,
    starts=10,
    keep_best=True,
    method=options_dict[w_choice_minimizer.value],
    steps=int(steps_fitting.value)
)

Showing the results

[16]:
problem.summarize().splitlines()
[16]:
['                                      DR |.........   0.119888 in (1e-12,2)',
 '                                  center .....|.... 0.00499981 in (-0.1,0.1)',
 '                                  radius ...|......    1.09193 in (0,3)',
 '                                   scale |.........    104.082 in (0,100000)',
 '                                  center ....|..... -0.00485342 in (-0.1,0.1)',
 '                                   scale |.........    106.017 in (0,100000)',
 '                                  center ....|..... -0.000758678 in (-0.1,0.1)',
 '                                   scale |.........    115.243 in (0,100000)',
 '                                  center .....|.... 0.00169753 in (-0.1,0.1)',
 '                                   scale |.........    130.916 in (0,100000)',
 '                                  center ....|..... -2.60542e-05 in (-0.1,0.1)',
 '                                   scale |.........    148.328 in (0,100000)',
 '                                  center ....|..... -0.00139538 in (-0.1,0.1)',
 '                                   scale |.........    173.884 in (0,100000)',
 '                                  center ....|..... -0.000519316 in (-0.1,0.1)',
 '                                   scale |.........     212.68 in (0,100000)',
 '                                  center ....|..... -0.00306572 in (-0.1,0.1)',
 '                                   scale |.........    267.616 in (0,100000)',
 '                                  center ....|..... -0.00459032 in (-0.1,0.1)',
 '                                   scale |.........    347.817 in (0,100000)',
 '                                  center ....|..... -0.00013493 in (-0.1,0.1)',
 '                                   scale |.........    473.339 in (0,100000)']
[17]:
# Print chi**2 and parameters' values after fit
print("final chisq", problem.chisq_str())
for k, v, dv in zip(problem.labels(), result.x, result.dx):
    if k in dict_physical_units.keys():
        print(k, ":", format_uncertainty_pm(v, dv), dict_physical_units[k])
    else:
        print(k, ":", format_uncertainty_pm(v, dv))
final chisq 0.9858(62)
DR : 0.11989 +/- 0.00099 meV
center : 0.004999811 +/- 0.000000020 meV
radius : 1.0919 +/- 0.0025 Angstrom
scale : 104.1 +/- 1.0 unit_of_signal.meV
center : -0.005 +/- 0.013 meV
scale : 106.0 +/- 1.1 unit_of_signal.meV
center : -0.8e-3 +/- 8.7e-3 meV
scale : 115.2 +/- 1.1 unit_of_signal.meV
center : 1.7e-3 +/- 7.2e-3 meV
scale : 130.9 +/- 1.2 unit_of_signal.meV
center : -0.00e9 +/- 0.10e9 meV
scale : 148.3 +/- 1.2 unit_of_signal.meV
center : -1.4e-3 +/- 4.4e-3 meV
scale : 173.9 +/- 1.4 unit_of_signal.meV
center : -0.5e-3 +/- 4.2e-3 meV
scale : 212.7 +/- 1.5 unit_of_signal.meV
center : -3.1e-3 +/- 3.2e-3 meV
scale : 267.6 +/- 1.7 unit_of_signal.meV
center : -4.6e-3 +/- 2.6e-3 meV
scale : 347.8 +/- 2.0 unit_of_signal.meV
center : -0.1e-3 +/- 3.0e-3 meV
scale : 473.3 +/- 2.4 unit_of_signal.meV

Display final configuration: experimental data, fitting model with output of fitting for the refined parameters

[18]:
slider1 = ipywidgets.IntSlider(value=0, min=0, max=len(q)-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 ({dict_physical_units['center']})$")
    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])
[18]:
[ ]:


Generated by nbsphinx from a Jupyter notebook.