Skip to content
Snippets Groups Projects
fitting.py 6.72 KiB
Newer Older
import scipy
Philipp Niedermayer's avatar
Philipp Niedermayer committed
import scipy.odr
import numpy as np


# Fitting
Philipp Niedermayer's avatar
Philipp Niedermayer committed
def fit_linear(S, V, **kwargs):
Philipp Niedermayer's avatar
Philipp Niedermayer committed
    """Fit a linear function to the data
    
    :param S: data x values
    :param V: data y values
    :param xerr: data x uncertainties
    :param yerr: data y uncertainties
    :param p0: initial values of parameters
    :param odr: Fit method to use: True means ortogonal distance regression (ODR), False means ordinary least square fit, None (default) uses ODR only if either xerr or yerr is not None
    :param extend: fraction by which the returned fit trace extends beyond the first/last data point
    """
Philipp Niedermayer's avatar
Philipp Niedermayer committed
    result = fit_function(lambda s, v0, m: v0+m*s, S, V, p0=(np.mean(V), np.mean(np.diff(V)/np.diff(S))), **kwargs)
    param, param_error, function, [X, _] = result
    return param, param_error, function, [X, function(X, *param)]

def fit_gaussian(S, V, **kwargs):
Philipp Niedermayer's avatar
Philipp Niedermayer committed
    """Fit a gaussian function to the data
    
    :param S: data x values
    :param V: data y values
    :param xerr: data x uncertainties
    :param yerr: data y uncertainties
    :param p0: initial values of parameters
    :param odr: Fit method to use: True means ortogonal distance regression (ODR), False means ordinary least square fit, None (default) uses ODR only if either xerr or yerr is not None
    :param extend: fraction by which the returned fit trace extends beyond the first/last data point
    """
    gauss = lambda s, v0, vp, s0, sigma: v0+vp*np.exp(-0.5*((s-s0)/sigma)**2)
    v0 = np.min(V)
    vp = np.max(V)-v0
    s0 = S[np.argmax(V)]
    sigma = np.sqrt(np.abs(np.sum((S - s0) ** 2 * V) / np.sum(V)))
    result = fit_function(gauss, S, V, p0=(v0, vp, s0, sigma), **kwargs)
    param, param_error, function, [X, _] = result
    param[3] = np.abs(param[3]) # return the positive sigma (could use bounds instead, but that's more likely to fail)
Philipp Niedermayer's avatar
Philipp Niedermayer committed
    return param, param_error, function, [X, function(X, *param)]
Philipp Niedermayer's avatar
Philipp Niedermayer committed
def fit_lorenzian(S, V, **kwargs):
    """Fit a lorenzian function to the data
    
    :param S: data x values
    :param V: data y values
    :param xerr: data x uncertainties
    :param yerr: data y uncertainties
    :param p0: initial values of parameters
    :param odr: Fit method to use: True means ortogonal distance regression (ODR), False means ordinary least square fit, None (default) uses ODR only if either xerr or yerr is not None
    :param extend: fraction by which the returned fit trace extends beyond the first/last data point
    """
    lorenzian = lambda s, v0, vp, s0, gamma: v0 + vp/(1+((s-s0)/gamma)**2)
    v0 = np.min(V)
    vp = np.max(V)-v0
    s0 = S[np.argmax(V)]
    sigma = np.sqrt(np.abs(np.sum((S - s0) ** 2 * (V-v0)) / np.sum(V-v0)))
    result = fit_function(lorenzian, S, V, p0=(v0, vp, s0, sigma), **kwargs)
    param, param_error, function, [X, _] = result
    param[3] = np.abs(param[3]) # return the positive sigma (could use bounds instead, but that's more likely to fail)
Philipp Niedermayer's avatar
Philipp Niedermayer committed
    return param, param_error, function, [X, function(X, *param)]


def fit_double_lorenzian(S, V, *, s0=None, dry_run=False, **kwargs):
    """Fit a lorenzian function to the data
    
    :param S: data x values
    :param V: data y values
    :param xerr: data x uncertainties
    :param yerr: data y uncertainties
    :param s0: initial peak positions
    :param p0: initial values of parameters
    :param odr: Fit method to use: True means ortogonal distance regression (ODR), False means ordinary least square fit, None (default) uses ODR only if either xerr or yerr is not None
    :param extend: fraction by which the returned fit trace extends beyond the first/last data point
    """
    lorenzian = lambda s, v0, vp, s0, gamma, vp2, s02, gamma2: v0 + vp/(1+((s-s0)/gamma)**2) + vp2/(1+((s-s02)/gamma2)**2)
    
    # ! for double peak fit, initial conditions are very important
    v0 = np.min(V)
    vp = np.max(V)-v0
    if s0 is None:
        s0 = S[np.argmax(V)]
        sigma = np.sqrt(np.abs(np.sum((S - s0) ** 2 * (V-v0)) / np.sum(V-v0)))/10
        s0 = [s0-3*sigma, s0+3*sigma]    
    sigma0 = np.sqrt(np.abs(np.sum((S - s0[0]) ** 2 * (V-v0)) / np.sum(V-v0)))/10
    sigma1 = np.sqrt(np.abs(np.sum((S - s0[1]) ** 2 * (V-v0)) / np.sum(V-v0)))/10
    p0=(v0, vp, s0[0], sigma0, vp, s0[1], sigma1)
    
    if dry_run:
        mi, ma = min(S), max(S)
        X = np.linspace(mi, ma, 1000)
        return p0, (0,0,0,0), 0, [X, lorenzian(X, *p0)]
        
    result = fit_function(lorenzian, S, V, p0=p0, **kwargs)
    param, param_error, function, [X, _] = result
    param[3] = np.abs(param[3]) # return the positive sigma (could use bounds instead, but that's more likely to fail)
    return param, param_error, function, [X, function(X, *param)]
Philipp Niedermayer's avatar
Philipp Niedermayer committed
def fit_function(function, x, y, *, xerr=None, yerr=None, p0=None, odr=None, extend=0):
    """Fit a function to the data
    
    :param function: fit function(x, *param)
    :param x: data x values
    :param y: data y values
    :param xerr: data x uncertainties
    :param yerr: data y uncertainties
    :param p0: initial values of parameters
    :param odr: Fit method to use: True means ortogonal distance regression (ODR), False means ordinary least square fit, None (default) uses ODR only if either xerr or yerr is not None
    :param extend: fraction by which the returned fit trace extends beyond the first/last data point
Philipp Niedermayer's avatar
Philipp Niedermayer committed
    if odr is None:
        odr = xerr is not None or yerr is not None
    
    if odr:
        # orthogonal distance regression
        data = scipy.odr.RealData(x, y, sx=xerr, sy=yerr)
        model = scipy.odr.Model(lambda beta, x: function(x, *beta))
        odr = scipy.odr.ODR(data, model, p0)
        # if not odr: odr.set_job(fit_type=2) # ordinary least-squares
        output = odr.run()
        param, param_error = output.beta, output.sd_beta
    else:
        # non-linear least squares
Philipp Niedermayer's avatar
Philipp Niedermayer committed
        param, cov = scipy.optimize.curve_fit(function, x, y, p0)
Philipp Niedermayer's avatar
Philipp Niedermayer committed
        param_error = np.sqrt(np.abs(cov.diagonal()))
    
Philipp Niedermayer's avatar
Philipp Niedermayer committed
    mi, ma = min(x), max(x)
    X = np.linspace(mi - (ma-mi)*extend, ma + (ma-mi)*extend, 1000)
    return param, param_error, function, [X, function(X, *param)]

def fit_exponential(S, V, **kwargs):
Philipp Niedermayer's avatar
Philipp Niedermayer committed
    """Fit an exponential function to the data
    
    :param S: data x values
    :param V: data y values
    :param xerr: data x uncertainties
    :param yerr: data y uncertainties
    :param p0: initial values of parameters
    :param odr: Fit method to use: True means ortogonal distance regression (ODR), False means ordinary least square fit, None (default) uses ODR only if either xerr or yerr is not None
    :param extend: fraction by which the returned fit trace extends beyond the first/last data point
    """
    exponential = lambda s, v0, vp, s0: v0 + vp*np.exp(s/s0)
    v0 = np.min(V)
    vp = np.max(V)-v0
    s0 = 1
    return fit_function(exponential, S, V, p0=(v0, vp, s0), **kwargs)