Basic fitting in EasyDynamics¶
We here show how to use EasyDynamics to fit a more advanced synthetic data set with multiple signals and a background, as well as an energy offset that could from from slight instrument misalignment.
The general procedure is to create an Experiment object to hold the data, a SampleModel to describe the model, and Analysis to fit the model to the data. The Analysis object now also contains an InstrumentModel, which describes the background and energy offset.
# Imports
import pooch
import easydynamics as edyn
import easydynamics.sample_model as sm
# Make the plots interactive
%matplotlib widget
As before, we createa an Experiment to hold the data, and rebin it.
# Load the data
experiment = edyn.Experiment(display_name='Tutorial')
file_path = pooch.retrieve(
url='https://github.com/easyscience/dynamics-lib/raw/refs/heads/tutorial0/docs/docs/tutorials/data/fake_advanced_data.hdf5',
known_hash='ee7310249df71a312ebc219f3e16b8da4e9aa37d29df919bbcaa541a38e1c39f',
)
experiment.load_hdf5(filename=file_path)
experiment.rebin({'Q': 16, 'energy': 256})
experiment.plot_data(slicer=True)
Downloading data from 'https://github.com/easyscience/dynamics-lib/raw/refs/heads/tutorial0/docs/docs/tutorials/data/fake_advanced_data.hdf5' to file '/home/runner/.cache/pooch/b435b6d060ac2c8248a84531ddd80995-fake_advanced_data.hdf5'.
This time, we see a sharp Gaussian near zero energy transfer, but with broad tails. There is also a pair of symmetrically placed peaks near +-1.5 meV. The background is not zero, and looks like it has a slope. We first focus on the three contributions to the signal: the Gaussian, its tails, which can be described by a Lorentzian, and the peaks near +- 1.5 meV, which can be described by a damped harmonic oscillator.
We create and place these in a ComponentCollection like this:
gaussian = sm.Gaussian(display_name='Gaussian', area=3, width=0.05)
lorentzian = sm.Lorentzian(display_name='Lorentzian', area=2, width=0.3)
dho = sm.DampedHarmonicOscillator(display_name='DHO', area=1.5, width=0.2, center=1.5)
collection = sm.ComponentCollection()
collection.append_component(gaussian)
collection.append_component(lorentzian)
collection.append_component(dho)
Note
model = sm.SampleModel(components=collection)
Next, we want to handle the two instrument effects. The first is the small shift in energy: if you zoom in on the peak at the center, you'll see that it is in fact not centered at zero - it is offset. The offset applies to elastic, inelastic and quasielastic scattering. This is very typical in real experiments, and can be caused by slight misalignment or imperfections in the instrument. To handle this, we create an InstrumentModel, and give it an energy_offset.
Note
instrument = sm.InstrumentModel(energy_offset=0.05)
Second, we want to handle the background. We do this with a BackgroundModel, which works the same as the SampleModel. We give it a Polynomial component:
💡 Tip
background = sm.BackgroundModel(components=sm.Polynomial(coefficients=[1.2, 0.05]))
We add the background to the instrument model:
💡 Tip
instrument.background_model = background
Now we create out Analysis object and give it the instrument model:
analysis = edyn.Analysis(
experiment=experiment,
sample_model=model,
instrument_model=instrument,
)
It is always good to check if the model somewhat matches the data.
analysis.plot_data_and_model()
As before, let us first fit a single $Q$ index and plot the data and model to see how it looks. We choose an arbitrary $Q$ and plot only that one
fit_result_independent_single_Q = analysis.fit(Q_index=5)
analysis.plot_data_and_model(Q_index=5)
The fit looks very good. We can again get a list of the parameters for this fit by accesing the corresponding Analysis1d object. Note ethat the Gaussian and Lorentzian centers are both zero, but that the energy_offset is non-zero.
analysis.analysis_list[5].get_all_parameters()
[<Parameter 'Gaussian area': 3.5594 ± 0.0229 meV, bounds=[0.0:inf]>, <Parameter 'Gaussian center': 0.0000 meV (fixed), bounds=[-inf:inf]>, <Parameter 'Gaussian width': 0.0503 ± 0.0003 meV, bounds=[1e-10:inf]>, <Parameter 'Lorentzian area': 10.3617 ± 0.0724 meV, bounds=[0.0:inf]>, <Parameter 'Lorentzian center': 0.0000 meV (fixed), bounds=[-inf:inf]>, <Parameter 'Lorentzian width': 0.3947 ± 0.0049 meV, bounds=[1e-10:inf]>, <Parameter 'DHO area': 2.5554 ± 0.0369 meV, bounds=[0.0:inf]>, <Parameter 'DHO center': 1.4832 ± 0.0013 meV, bounds=[1e-10:inf]>, <Parameter 'DHO width': 0.0978 ± 0.0019 meV, bounds=[1e-10:inf]>, <Parameter 'energy_offset': 0.1007 ± 0.0002 meV, bounds=[-inf:inf]>, <Parameter 'Polynomial_c0': 1.2031 ± 0.0150, bounds=[-inf:inf]>, <Parameter 'Polynomial_c1': 0.1958 ± 0.0047, bounds=[-inf:inf]>]
Since the fit looked good, we can now fit all $Q$. We also plot the result, again using the slicer.
fit_result_all_Q = analysis.fit()
analysis.plot_data_and_model()
Let us plot a few parameters as a function of Q using the plot_parameters method.
analysis.plot_parameters(names=['Gaussian area', 'Lorentzian area', 'DHO area'])