# Fitting X-ray Absorbance Spectra¶

When numerical methods are insufficient, it may be necessary to fit
the pixel spectra with a model function and extract parameters from
the model. The core `XanesFrameset`

class has methods for common fitting-related use-cases, such as using
curves to approximate L_{3} and K edges, and linear
combination fitting of standard spectra. If the pre-rolled options are
not enough, arbitary callables can be created and fit against the
data. The following is an example for fitting a single L-edge
spectrum:

```
Es = np.linspace(845, 865, num=1000)
obs = ... # Load your data
l3 = xp.fitting.L3Curve(Es, num_peaks=2)
# Create initial guess, matching ``l3.param_names``
p0 = (1.1, 853, 0.6, 1, 855, 0.6, 0.15, 854, 10, 3)
# Now do the fitting
result = xp.fitting.fit_spectra(observations=obs, func=l3, p0=p0)
params, residuals = result
# Plot the resulting fit and original data
predicted = l3(*params)
plt.plot(Es, obs)
plt.plot(Es, predicted, linestyle=':')
```

Todo

Come up with a better illustration for fitting.

Note

If no *p0* is given to
`fit_spectra()`

, xanespy
will attempt to guess starting parameters. Not every callable in
`xanespy.fitting`

supports this feature. If trying to fit
spectra without *p0* raises a
`GuessParamsError`

, then *p0* is
required.

## Linear Combination Fitting¶

Often times the observed spectrum is a linear combination of spectral sources from known standards. This works best when the standards is stable and have been isolated and measured on the same instrument as the observed data.

In order the fit linear combinations of source, use the
`fit_linear_combinations()`

method:

```
# Load your previously imported data
fs = xp.XanesFrameset(...)
# Prepare the sources for fitting
source1 = ...
source2 = ...
# The sources must have the same number of points as the data
assert len(source1) == fs.num_energies
assert len(source2) == fs.num_energies
# Now execute the fitting
results = fs.fit_linear_combinations(sources=[source1, source2])
fits, residuals = results
```

Todo

Create a figure to illustrate LC fitting.

This method will create three new HDF5 datasets:

- linear_combination_parameters (maps)
- linear_combination_residuals (maps)
- linear_combination_sources (arrays)

The naming prefix can be controlled by passing the `name`

parameter
to
`fit_linear_combinations()`

method. If more control is needed, the
`xanespy.fitting.LinearCombination`

class can be subclassed
and given to the
`fit_spectra()`

method as
described below.

## Shortcuts for Common Use-Cases¶

The `XanesFrameset`

class has
several shortcuts for common fitting tasks. Fitting the spectra with a
K-edge spectra can be done easily with the
`fit_kedge()`

method. Linear combinations of existing functions can be easily fit
using
`fit_linear_combinations()`

.

## Rolling Your Own Fit Function¶

If none of the options suit your needs, you can create a callable that
produces the curve you wish to fit given a number of parameters, then
pass this to the
`fit_spectra()`

method. In the simplest case this can be a simple function:

```
import numpy as np
import xanespy as xp
# Define the function we wish to fit against
def sin_curve(scale, frequency, phase):
theta = np.linspace(0, 2*np.pi, num=100)
out = scale * np.sin(frequency * theta(phase))
return out
fs = xp.XanesFrameset(...)
# Come up with an initial guess
pnames = ('scale', 'frequency', 'phase')
p0 = (0, 1, 0)
fs.fit_spectra(func=sin_curve, p0=p0, pnames=pnames, name='sin_curve')
```

In many cases, static information (such as the list of energies) is
needed to construct the curve. This can be given to a class’s
constructor and the algorithm itself placed in the `__call__`

method. This is illustrated below by fitting a variable number of sine
waves, making a sort of horribly inefficient fourier transform. Since
the number of sine waves is not known at import-time, the use of
star-arguments makes the result more dynamic. Adding the
`param_names`

saves us the trouble of passing it in every
time. Providing a `guess_params`

method allows
`fit_spectra`()`

to
automatically guess the parameters for each spectrum before fitting.

```
import random
import xanespy as xp
import numpy as np
# Define a new callable for passing to the fitting function
class SineCurves(xp.fitting.Curve):
def __init__(self, theta, num_sines=1):
self.theta = theta
self.num_sines = num_sines
def __call__(self, *params):
out = np.zeros_like(self.theta)
# Iterate on the parameters in groups of 3
for i in xrange(0, len(params), 3):
scale, freq, phase = params[i:i+3]
# Add another sin wave to the total curve
out += scale * np.sin((self.theta-phase) * frequency)
return out
@property
def param_names(self):
# Build a list of 2 params for each sine wave
names = []
for num in range(self.num_sines):
names.append('scale%d' % num)
names.append('frequency%d' % num)
names.append('phase%d' % num)
return names
def guess_params(self, intensities, edge, named_tuple=True):
# To start with, guess sensible parameters for each sine wave
p0 = []
for i in range(self.num_sines):
p0 += [1, 2*i+1, 0]
# Convert to named tuple for user convenience (optional)
if named_tuple:
Params = namedtuple('Params', self.param_names)
p0 = Params(*p0)
else:
p0 = tuple(p0)
return p0
# Create the actual callable object
theta = np.linspace(0, 2*pi, num=100)
sines = SineCurves(theta=theta, num_sines=3)
# Load the data and do the fitting
fs = xp.XanesFrameset(...)
fs.fit_spectra(func=sines, p0=p0, name='sine_curve_fit')
```