module mod_nlfit
  use fgsl
  use, intrinsic :: iso_c_binding
  implicit none
  type data
     integer(fgsl_size_t) :: n
     real(fgsl_double), allocatable :: y(:)
  end type data
contains
  function expb_f(x, cdata, f) bind(c)
    type(c_ptr), value :: x, cdata, f
    integer(c_int) :: expb_f
!
    type(fgsl_vector) :: f_x, f_f
    type(data), pointer :: f_data
    integer(fgsl_size_t) :: i
    integer(fgsl_int) :: status
    real(fgsl_double) :: yy
    real(fgsl_double), pointer :: p_x(:), p_f(:)
!
!   cast *void to an fgsl_vector
    call fgsl_obj_c_ptr(f_x, x)
    call fgsl_obj_c_ptr(f_f, f)
!
!   align fgsl_vector with Fortran POINTER
    status = fgsl_vector_align(p_x, f_x)
    status = fgsl_vector_align(p_f, f_f)
!
!   cast *void to Fortran data type
    call c_f_pointer(cdata, f_data)
    do i=1,f_data%n
       yy = p_x(1) * exp(- p_x(2) * dble(i-1) ) + p_x(3)
       p_f(i) = (yy - f_data%y(i))
    end do
    expb_f = fgsl_success
  end function expb_f
  function expb_df(x, cdata, j) bind(c)
    type(c_ptr), value :: x, cdata, j
    integer(c_int) :: expb_df
!
    type(fgsl_vector) :: f_x
    type(fgsl_matrix) :: f_j
    type(data), pointer :: f_data
    integer(fgsl_size_t) :: i
    integer(fgsl_int) :: status
    real(fgsl_double) :: yy
    real(fgsl_double), pointer :: p_x(:), p_j(:,:)
!
    call fgsl_obj_c_ptr(f_x, x)
    call fgsl_obj_c_ptr(f_j, j)
    status = fgsl_vector_align(p_x, f_x)
    status = fgsl_matrix_align(p_j, f_j)
    call c_f_pointer(cdata, f_data)
    do i=1,f_data%n
       yy = exp(- p_x(2) * dble(i-1) )
       p_j(1,i) = yy 
       p_j(2,i) = - dble(i-1) * p_x(1) * yy 
       p_j(3,i) = 1.0_fgsl_double
    end do
    expb_df = fgsl_success
  end function expb_df
end module
program nlfit
  use mod_nlfit
  implicit none
  real(fgsl_double), parameter :: eps = 1.0E-8_fgsl_double
  integer(fgsl_size_t), parameter :: itmax = 30
  integer(fgsl_size_t), parameter :: nmax = 40, nrt = 3
  integer(fgsl_int) :: status
  type(fgsl_multifit_nlinear_type) :: t
  type(fgsl_multifit_nlinear_workspace) :: nlfit_w
  type(fgsl_multifit_nlinear_parameters) :: fdf_params
  type(fgsl_multifit_nlinear_fdf) :: nlfit_fdf
  type(fgsl_rng) :: rng
  type(fgsl_vector) :: params, wts
  type(fgsl_matrix) :: cov, jac
  type(data), target :: fitdata
  type(c_ptr) :: ptr
  integer(fgsl_int) :: info
  real(fgsl_double), target :: v_params(nrt), v_cov(nrt,nrt), v_jac(nrt,nmax), v_wts(nmax)
  real(fgsl_double), pointer :: v_fun(:), v_parptr(:)
  real(fgsl_double) :: chi, c, dy, si, xtol, gtol, ftol
  integer :: i
!
  rng = fgsl_rng_alloc(fgsl_rng_default)
  fitdata%n = nmax
  allocate(fitdata%y(nmax))
  write(*, fmt='('' No.       Data value       Sigma'')')
  do i=1, nmax
     fitdata%y(i) = 1.0_fgsl_double + &
          5.0_fgsl_double * exp(-0.1_fgsl_double*dble(i-1)) 
     si = 0.1_fgsl_double * fitdata%y(i)
     dy = fgsl_ran_gaussian(rng, si)
     v_wts(i) = 1.0_fgsl_double / (si * si)
     fitdata%y(i) = fitdata%y(i) + dy
     write(*, fmt='(I3,2X,2(F16.8,1X))') i, fitdata%y(i), si
  end do
  wts = fgsl_vector_init(v_wts)
!
! set up default values for solver parameters, initialize function object
! and solver workspace
  t = fgsl_multifit_nlinear_type('trust')
  fdf_params = fgsl_multifit_nlinear_default_parameters()
  ptr = c_loc(fitdata)
  nlfit_fdf = fgsl_multifit_nlinear_fdf_init(nmax, nrt, ptr, expb_f, expb_df)
  nlfit_w = fgsl_multifit_nlinear_alloc(t, fdf_params, nmax, nrt)
  write(*, '(''Default solver name: '',A)') fgsl_multifit_nlinear_name(nlfit_w)
  write(*, '(''Default trust method: '',A)') fgsl_multifit_nlinear_trs_name(nlfit_w)
!
! initial guess for fit parameters
  v_params(1:3) = (/1.0_fgsl_double, 0.0_fgsl_double, 0.0_fgsl_double/)
  params = fgsl_vector_init(v_params)
! alignment of target function and parameter values only needed once
! storage is persistently allocated within the working spade
  v_fun => fgsl_vector_to_fptr(fgsl_multifit_nlinear_residual(nlfit_w))
  v_parptr => fgsl_vector_to_fptr(fgsl_multifit_nlinear_position(nlfit_w))
! storage for cov within Fortran
  cov = fgsl_matrix_init(v_cov)
  jac = fgsl_matrix_init(v_jac)
  if (fgsl_well_defined(nlfit_w)) then
!
! initialize solver with starting points and weights
     status = fgsl_multifit_nlinear_winit(params, wts, nlfit_fdf, nlfit_w)
! tolerances
     xtol = eps; gtol = eps; ftol = 0.0_fgsl_double
     i = 0
     write(*, fmt= &
          '(''Iteration   <--------- Fit Parameter Values --------->   Targ. function'')')
     do
        write(*, fmt='(2X,I3,2X,3(F15.8,1X),2X,1PD12.5)') i, v_parptr(1:nrt), &
             sqrt(dot_product(v_fun,v_fun))
        i = i + 1
        status = fgsl_multifit_nlinear_iterate(nlfit_w)
        if (status /= fgsl_success .or. i > itmax) then
           write(*, *) 'Iteration or Convergence failure'
           exit
        end if
        status = fgsl_multifit_nlinear_test( xtol, gtol, ftol, info, nlfit_w)
        if (status == fgsl_success) then
           exit
        end if
     end do
     jac = fgsl_multifit_nlinear_jac(nlfit_w)
     status = fgsl_multifit_nlinear_covar(jac, 0.0_fgsl_double, cov)
     chi = sqrt(dot_product(v_fun,v_fun))
     c = max(1.0d0, chi/sqrt(dble(nmax-nrt)))
!  c only relevant for error estimate if chi*chi/dof exceeds 1
     write(*, '(''Iteration complete. Final Results for '')')
     write(*, '(''  fit function A exp (-lambda x) + b: '')')
     write(*, '(''  A      = '',1PE15.8,'' +/- '',1PE12.5)') v_parptr(1), &
          c * sqrt(v_cov(1,1))
     write(*, '(''  lambda = '',1PE15.8,'' +/- '',1PE12.5)') v_parptr(2), &
          c * sqrt(v_cov(2,2))
     write(*, '(''  b      = '',1PE15.8,'' +/- '',1PE12.5)') v_parptr(3), &
          c * sqrt(v_cov(3,3))
     write(*, '(''  chisq / degrees_of_freedom: '',1PE10.3)') chi*chi/dble(nmax-nrt)
  end if
  call fgsl_vector_free(params)
  call fgsl_matrix_free(cov)
  call fgsl_multifit_nlinear_free(nlfit_w)
  call fgsl_multifit_nlinear_fdf_free(nlfit_fdf)
  call fgsl_rng_free(rng)
end program nlfit
