Search code examples
c++curve-fittinggsl

Nonlinear least-squares fitting with two independent variables in C++: implementing GSL algorithm


Following up to a previous question I asked in Fixing parameters of a fitting function in Nonlinear Least-Square GSL (successfully answered by @zkoza), I would like to implement an algorithm that can fit data to a non-linear function, by fixing some of its parameters while leaving other parameters to change for finding the best fit to the data. The difference to my previous question is that I want to have two independent variables instead of one independent variable.

Non-linear function used to fit the data

double gaussian(double x, double b, double a, double c)
{
    const double z = (x - b) / c;
    return a * std::exp(-0.5 * z * z);
}

In my previous question I was considering that x was the only independent variable. Now I would like to consider two independent variables, x and b.

The original algorithm used to fit a non-linear function using only one independent variable (while fixing variable a) is a C++ wrapper of the GSL nonlinear least-squares algorithm (borrowed from https://github.com/Eleobert/gsl-curve-fit/blob/master/example.cpp):

template <typename F, size_t... Is>
auto gen_tuple_impl(F func, std::index_sequence<Is...> )
{
    return std::make_tuple(func(Is)...);
}

template <size_t N, typename F>
auto gen_tuple(F func)
{
    return gen_tuple_impl(func, std::make_index_sequence<N>{} );
}

template <class R, class... ARGS>
struct function_ripper {
    static constexpr size_t n_args = sizeof...(ARGS);
};

template <class R, class... ARGS>
auto constexpr n_params(R (ARGS...) )
{
    return function_ripper<R, ARGS...>();
}


auto internal_solve_system(gsl_vector* initial_params, gsl_multifit_nlinear_fdf *fdf,
             gsl_multifit_nlinear_parameters *params) -> std::vector<double>
{
  // This specifies a trust region method
  const gsl_multifit_nlinear_type *T = gsl_multifit_nlinear_trust;
  const size_t max_iter = 200;
  const double xtol = 1.0e-8;
  const double gtol = 1.0e-8;
  const double ftol = 1.0e-8;

  auto *work = gsl_multifit_nlinear_alloc(T, params, fdf->n, fdf->p);
  int info;

  // initialize solver
  gsl_multifit_nlinear_init(initial_params, fdf, work);
  //iterate until convergence
  gsl_multifit_nlinear_driver(max_iter, xtol, gtol, ftol, nullptr, nullptr, &info, work);

  // result will be stored here
  gsl_vector * y    = gsl_multifit_nlinear_position(work);
  auto result = std::vector<double>(initial_params->size);

  for(int i = 0; i < result.size(); i++)
  {
    result[i] = gsl_vector_get(y, i);
  }

  auto niter = gsl_multifit_nlinear_niter(work);
  auto nfev  = fdf->nevalf;
  auto njev  = fdf->nevaldf;
  auto naev  = fdf->nevalfvv;

  // nfev - number of function evaluations
  // njev - number of Jacobian evaluations
  // naev - number of f_vv evaluations
  //logger::debug("curve fitted after ", niter, " iterations {nfev = ", nfev, "} {njev = ", njev, "} {naev = ", naev, "}");

  gsl_multifit_nlinear_free(work);
  gsl_vector_free(initial_params);
  return result;
}

template<auto n>
auto internal_make_gsl_vector_ptr(const std::array<double, n>& vec) -> gsl_vector*
{
    auto* result = gsl_vector_alloc(vec.size());
    int i = 0;
    for(const auto e: vec)
    {
        gsl_vector_set(result, i, e);
        i++;
    }
    return result;
}


template<typename C1>
struct fit_data
{
    const std::vector<double>& t;
    const std::vector<double>& y;
    // the actual function to be fitted
    C1 f;
};


template<typename FitData, int n_params>
int internal_f(const gsl_vector* x, void* params, gsl_vector *f)
{
    auto* d  = static_cast<FitData*>(params);
    // Convert the parameter values from gsl_vector (in x) into std::tuple
    auto init_args = [x](int index)
    {
        return gsl_vector_get(x, index);
    };
    auto parameters = gen_tuple<n_params>(init_args);

    // Calculate the error for each...
    for (size_t i = 0; i < d->t.size(); ++i)
    {
        double ti = d->t[i];
        double yi = d->y[i];
        auto func = [ti, &d](auto ...xs)
        {
            // call the actual function to be fitted
            return d->f(ti, xs...);
        };
        auto y = std::apply(func, parameters);
        gsl_vector_set(f, i, yi - y);
    }
    return GSL_SUCCESS;
}

using func_f_type   = int (*) (const gsl_vector*, void*, gsl_vector*);
using func_df_type  = int (*) (const gsl_vector*, void*, gsl_matrix*);
using func_fvv_type = int (*) (const gsl_vector*, const gsl_vector *, void *, gsl_vector *);

template<auto n>
auto internal_make_gsl_vector_ptr(const std::array<double, n>& vec) -> gsl_vector*;


auto internal_solve_system(gsl_vector* initial_params, gsl_multifit_nlinear_fdf *fdf,
             gsl_multifit_nlinear_parameters *params) -> std::vector<double>;

template<typename C1>
auto curve_fit_impl(func_f_type f, func_df_type df, func_fvv_type fvv, gsl_vector* initial_params, fit_data<C1>& fd) -> std::vector<double>
{
    assert(fd.t.size() == fd.y.size());

    auto fdf = gsl_multifit_nlinear_fdf();
    auto fdf_params = gsl_multifit_nlinear_default_parameters();

    fdf.f   = f;
    fdf.df  = df;
    fdf.fvv = fvv;
    fdf.n   = fd.t.size();
    fdf.p   = initial_params->size;
    fdf.params = &fd;

    // "This selects the Levenberg-Marquardt algorithm with geodesic acceleration."
    fdf_params.trs = gsl_multifit_nlinear_trs_lmaccel;
    return internal_solve_system(initial_params, &fdf, &fdf_params);
}


template <typename Callable, auto n>
auto curve_fit(Callable f, const std::array<double, n>& initial_params, const std::vector<double>& x, const std::vector<double>& y) -> std::vector<double>
{
    // We can't pass lambdas without convert to std::function.
    //constexpr auto n = 3;//decltype(n_params(f))::n_args - 5;
    //constexpr auto n = 2;
    assert(initial_params.size() == n);

    auto params = internal_make_gsl_vector_ptr(initial_params);
    auto fd = fit_data<Callable>{x, y, f};
    return curve_fit_impl(internal_f<decltype(fd), n>, nullptr, nullptr, params,  fd);
}

In order to fix one of the parameters of the gaussian function, @zkoza proposed to use functors:

struct gaussian_fixed_a
{
    double a;
    gaussian_fixed_a(double a) : a{a} {}
    double operator()(double x, double b, double c) const { return gaussian(x, b, a, c); }
};

And these last lines show how I would create a fake dataset of observed data (with some noise which is normally distributed) and test the fitting curve function with two independent variables, given by the vectors xs and bs.

    int main()
    {
        auto device = std::random_device();
        auto gen    = std::mt19937(device());
    
        auto xs = linspace<std::vector<double>>(0.0, 1.0, 300);
        auto bs = linspace<std::vector<double>>(0.4, 1.4, 300);
        auto ys = std::vector<double>(xs.size());
    
        double a = 5.0, c = 0.15;
    
        for(size_t i = 0; i < xs.size(); i++)
        {

            auto y =  gaussian(xs[i], a, bs[i], c);
            auto dist  = std::normal_distribution(0.0, 0.1 * y);
            ys[i] = y + dist(gen);
        }
        gaussian_fixed_a g(a);
        auto r = curve_fit(g, std::array{0.11}, xs, bs, ys);
    
        std::cout << "result: " << r[0] << ' ' << '\n';
        std::cout << "error : " << r[0] - c << '\n';
    
    }

Do you have any idea on how I could implement the two-independent variables non-linear fitting?


Solution

  • The solution, as suggested in the comments by @BenVoigt, is to replace the x and b independent variables in the gaussian function with 'one independent variable' given as a vector, whose first element is x and the second element is b.

    Also the backbone of the nonlinear fitting needs to be slightly edited. The edits consist:

    1. Replace the fit_data functor with:

      struct fit_data
       {
           const std::vector< vector<double> > &t;
           const std::vector<double>& y;
           // the actual function to be fitted
           C1 f;
       };
      

    Such that, the independent variable is no longer a vector but rather a vector of a vector (aka a matrix).

    1. Replace within the function internal_f.

    a) double ti = d->t[i] with std::vector<double> ti = d->t[i] b) auto func = [ti, &d](auto ...xs) with auto func = [ti, &d](auto ...xs_matrix) c) return d->f(ti, xs...) with return d->f(ti, xs_matrix...)

    1. Replace within curve_fit function:

    a) const std::vector<double>& x with const std::vector< vector<double> > &xs_matrix b) auto fd = fit_data<Callable>{x, y, f} with auto fd = fit_data<Callable>{xs_matrix, y, f}

    Whereas the gaussian function, gaussian_fixed_a functor and the fitting function looks like:

    double gaussian(std::vector<double> x_vector, double a, double c)
        {
            const double z = (x_vector[0] - x_vector[1]) / c;
            return a * std::exp(-0.5 * z * z);
        }
    
    struct gaussian_fixed_a
    {
        double a;
        gaussian_fixed_a(double a) : a{a} {}
        double operator()(std::vector<double> x_vector, double c) const { return gaussian(x_vector, a, c); }
    };
    
    double fittingTest(const std::vector< vector<double> > &xs_matrix, const std::vector<double> ys, const double a){
    
          gaussian_fixed_a g(a);
          auto r = curve_fit(g, std::array{3.0}, xs_matrix, ys);
    
            return r[0]);
    
            }