Writing Your Own Fitting Function

When Should You Do It?

Even though Eddington offers many default fitting functions, sometimes you may want to customize your own fitting function.

Consider the following case: You conduct an experiment to demonstrate the Thin Lens Equation:

\frac{1}{u}+\frac{1}{v} = \frac{1}{f}

After rearranging the equation you get:

v = \frac{uf}{u - f}

You have data records of v and u and you want to estimate f. You can use our hyperbolic fitting function, but in order to find f You’ll have to do some more calculations with respect to the errors of the parameters you’ve found.

A somewhat easier approach would be to use the following fitting function:

y=\frac{a_0x}{x-a_0}+a_1

Now, after you fit the data, you get f directly (which equals to a_0)

Since this fitting function is not implemented by default, you’d have to implement it yourself.

Basic implementation

A basic implementation of the fitting function presented in the example above would look like this:

from eddington import fitting_function

@fitting_function(n=2)
def lens(a, x):
    return (a[0] * x) / (x - a[0]) + a[1]

We wrap the lens fitting function with the fitting_function decorator in order to indicate that this function is actually a fitting function. the n variable indicates how many parameters the fitting function expects. In our example, we expect 2 parameters: a[0] which is f, and a[1] which encapsulates the systematic errors in our v samples.

Note

The inputs of the fitting function are a which is the parameters vector and x which is the free variable. While a can be from any array-like type such as list, tuple, numpy.ndarray, etc. x can be both a numpy.ndarray and float.

Now, we can use the fitting function we’ve created in order to fit the data:

from eddington import FittingData, fit

fitting_data = FittingData.read_from_csv("/path/to/data.csv")  # Load data from file.
fitting_result = fit(fitting_data, lens)  # Do the actual fitting
print(fitting_result)  # Print the results

This usage is more than enough for most use-cases.

Derivatives

Sometimes, you wish to get an accurate fit, and fast. One way to achieve that is to add derivatives to the fitting function. In our example, we have the following derivatives:

x derivative -

\frac{\partial y}{\partial x}=-\frac{a_0^2}{(x-a_0)^2}

a_0 derivative -

\frac{\partial y}{\partial a_0}=\frac{x^2}{(x-a_0)^2}

a_1 derivative -

\frac{\partial y}{\partial a_1}=1

In order to add those derivatives to the fitting function, we should add the x_derivative and a_derivative to the fitting_function decorator. In our example:

import numpy as np
from eddington import fitting_function, FittingData, fit


@fitting_function(
    n=2,
    x_derivative=lambda a, x: -np.power(a[0], 2) / np.power(x - a[0], 2),
    a_derivative=lambda a, x: np.stack(
        [
            np.power(x, 2) / np.power(x - a[0], 2),
            np.ones(shape=np.shape(x)),
        ]
    ),
)
def lens(a, x):
    return (a[0] * x) / (x - a[0]) + a[1]

Note

When implementing the derivatives pay attention that you take a as the first parameter and x as the second. Moreover, make sure that the dimension of the output x_derivative returns a numpy.ndarray with dimension similar to x, while a_derivative returns a numpy.ndarray with dimension equal to x dimension times a dimension.

The Fitting Functions Registry

By default, creating a new fitting function adds it automatically to the FittingFunctionsRegistry, a singleton containing all fitting functions. Once the fitting function you’ve created is imported (for example, in the __init__.py file) it can be loaded from the registry in the following way:

from eddington import FittingFunctionsRegistry

fit_func = FittingFunctionsRegistry.load("lens")

If you wish to specify a different name to the fitting function by which it can be loaded from the registry, use the name parameter in the fitting_function decorator in the following way:

from eddington import fitting_function, FittingFunctionsRegistry

@fitting_function(n=2, name="my_amazing_func")
def lens(a, x):
    return (a[0] * x) / (x - a[0]) + a[1]

fit_func = FittingFunctionsRegistry.load("my_amazing_func")  # Returns the "lens" function

If you expect others to use your new fitting function, consider adding a syntax string indicating how the fitting functions fit the data. This can be printed out when needed. For example:

from eddington import fitting_function, FittingFunctionsRegistry

@fitting_function(n=2, syntax="(a[0] * x) / (x - a[0]) + a[1]")
def lens(a, x):
    return (a[0] * x) / (x - a[0]) + a[1]

...

fit_func = FittingFunctionsRegistry.load("lens")
print(f"Syntax is: {fit_func.syntax}")  # Prints out the defined syntax

Lastly, if you wish the fitting function to not be saved into the registry, specify save=False in the fitting_function decorator. For example:

from eddington import fitting_function

@fitting_function(n=2, save=False)
def lens(a, x):
    return (a[0] * x) / (x - a[0]) + a[1]

As mentioned earlier, by default save is set to True.

Warning

Two functions cannot be saved into the registry under the same name. Make sure that every new fitting function you write has a unique name, which is not one of the default fitting functions or another custom fitting function you expect to use in your code.

Using External Packages

When writing a custom fitting function, one can use external packages such as numpy and scipy in the fitting code. Here is an example:

import numpy as np
from eddington import fitting_function

@fitting_function(n=2)
def test(a, x):
    return a[0] / np.sqrt(x - a[0])

In that way you can make more complex fitting functions.

Note

The optimization algorithm uses numpy.array in order to make parallel calculations. Therefore, using the built-in math package will fail most of the times. In order to solve the problem, use numpy methods (which do excepts arrays) instead.