GithubHelp home page GithubHelp logo

jacobwilliams / roots-fortran Goto Github PK

View Code? Open in Web Editor NEW
34.0 6.0 5.0 1.72 MB

A modern Fortran library for finding the roots of continuous scalar functions of a single real variable, using derivative-free methods.

License: Other

Fortran 100.00%
root-finding brent-dekker bisection regula-falsi fortran muller zeroin muller-s-method fortran-package-manager

roots-fortran's Introduction

roots-fortran

roots-fortran: root solvers for modern Fortran

Language GitHub release CI Status codecov last-commit

Description

A modern Fortran library for finding the roots of continuous scalar functions of a single real variable.

Compiling

A fpm.toml file is provided for compiling roots-fortran with the Fortran Package Manager. For example, to build:

fpm build --profile release

By default, the library is built with double precision (real64) real values. Explicitly specifying the real kind can be done using the following processor flags:

Preprocessor flag Kind Number of bytes
REAL32 real(kind=real32) 4
REAL64 real(kind=real64) 8
REAL128 real(kind=real128) 16

For example, to build a single precision version of the library, use:

fpm build --profile release --flag "-DREAL32"

To run the unit tests:

fpm test

To use roots-fortran within your fpm project, add the following to your fpm.toml file:

[dependencies]
roots-fortran = { git="https://github.com/jacobwilliams/roots-fortran.git" }

or, to use a specific version:

[dependencies]
roots-fortran = { git="https://github.com/jacobwilliams/roots-fortran.git", tag = "1.0.0"  }

To generate the documentation using ford, run: ford roots-fortran.md

Usage

Methods

The module contains the following methods:

Procedure Description Reference
bisection Classic bisection method Bolzano (1817)
regula_falsi Classic regula falsi method ?
muller Improved Muller method (for real roots only) Muller (1956)
brent Classic Brent's method (a.k.a. Zeroin) Brent (1971)
brenth SciPy variant of brent SciPy
brentq SciPy variant of brent SciPy
illinois Illinois method Dowell & Jarratt (1971)
pegasus Pegasus method Dowell & Jarratt (1972)
anderson_bjorck Anderson-Bjorck method King (1973)
anderson_bjorck_king a variant of anderson_bjorck King (1973)
ridders Classic Ridders method Ridders (1979)
toms748 Algorithm 748 Alefeld, Potra, Shi (1995)
chandrupatla Hybrid quadratic/bisection algorithm Chandrupatla (1997)
bdqrf Bisected Direct Quadratic Regula Falsi Gottlieb & Thompson (2010)
zhang Zhang's method (with corrections from Stage) Zhang (2011)
itp Interpolate Truncate and Project method Oliveira & Takahashi (2020)
barycentric Barycentric interpolation method Mendez & Castillo (2021)
blendtf Blended method of trisection and false position Badr, Almotairi, Ghamry (2021)

In general, all the methods are guaranteed to converge. Some will be more efficient (in terms of number of function evaluations) than others for various problems. The methods can be broadly classified into three groups:

  • Simple classical methods (bisection, regula_falsi, illinois, ridders).
  • Newfangled methods (zhang, barycentric, blendtf, bdqrf, anderson_bjorck_king). These rarely or ever seem to be better than the best methods.
  • Best methods (anderson_bjorck, muller, pegasus, toms748, brent, brentq, brenth, chandrupatla, itp). Generally, one of these will be the most efficient method.

Note that some of the implementations in this library contain additional checks for robustness, and so may behave better than naive implementations of the same algorithms. In addition, all methods have an option to fall back to bisection if the method fails to converge.

Functional Interface Example

program main

  use root_module, wp => root_module_rk

  implicit none

  real(wp) :: x, f
  integer :: iflag

  call root_scalar('bisection',func,-9.0_wp,31.0_wp,x,f,iflag)

  write(*,*) 'f(',x,') = ', f
  write(*,*) 'iflag    = ', iflag

contains

  function func(x) result(f)

  implicit none

  real(wp),intent(in) :: x
  real(wp) :: f

  f = -200.0_wp * x * exp(-3.0_wp*x)

  end function func

end program main

Object Oriented Interface Example

program main

  use root_module, wp => root_module_rk

  implicit none

  type,extends(bisection_solver) :: my_solver
  end type my_solver

  real(wp) :: x, f
  integer :: iflag
  type(my_solver) :: solver

  call solver%initialize(func)
  call solver%solve(-9.0_wp,31.0_wp,x,f,iflag)

  write(*,*) 'f(',x,') = ', f
  write(*,*) 'iflag    = ', iflag

contains

  function func(me,x)

    class(root_solver),intent(inout) :: me
    real(wp),intent(in) :: x
    real(wp) :: f

    f = -200.0_wp * x * exp(-3.0_wp*x)

  end function func

end program main

Result

 f( -2.273736754432321E-013 ) =   4.547473508867743E-011
 iflag    =            0

Notes

Documentation

The latest API documentation for the master branch can be found here. This was generated from the source code using FORD.

License

The roots-fortran source code and related files and documentation are distributed under a permissive free software license (BSD-style).

Related Fortran libraries

  • polyroots-fortran -- For finding all the roots of polynomials with real or complex coefficients.

Similar libraries in other programming languages

Language Library
C GSL
C++ Boost Math Toolkit
Julia Roots.jl
R uniroot
Rust roots
MATLAB fzero
Python scipy.optimize.root_scalar

References

See also

roots-fortran's People

Contributors

ivan-pi avatar jacobwilliams avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

roots-fortran's Issues

Use nopass to simplify the interface for the user-defined function

The abstract interface func has two arguments, "me" of type "class(root_solver)" and the proper argument of the function. The first serves no purpose for the evaluation of the function and can actually be suppressed:

procedure(func),pointer, nopass :: f => null()  !! user function to find the root of

This way the interface becomes the same as func2 and unifies the usage.

Robust sign comparison

if ((isign(fa)*isign(fc)) < 0) then

I noticed the sign comparison is performed inconsistently; i.e. in some places as fa*fc < 0 and in some places you extract the sign explicitly like above.

The latter option is in agreement with what Kahaner, Moler, & Nash have to say about this:

image

The original SLATEC FZERO from Shampine & Watts does it like this:

IF (SIGN(1.0E0,FB) .NE. SIGN(1.0E0,FC)) ...

Leaving the underflow issues aside, the sign version seems to generate a few instructions less than your branching isign function. Moreover, gfortran and ifx produce branchless instructions when using the sign intrinsic: https://godbolt.org/z/E41P9c4Mz

As a matter of clarity, the sign comparison could also be abstracted as a function or operator, e.g.

if (sne(fb,fc)) ...  ! sign not equal

or

if( fb .oppsign. fc) ...   ! opposite sign

but I guess this is more a matter of taste (I'm not advocating for it, but the compilers seem able to inline such operators just fine).

itp subroutine failure on inputs

Good day,

The itp subroutine fails for the following arguments:

ax=0
bx=157.08
f(ax)=1012
f(bx)=-1114

these values result in the variable "term" on line 2536 of root_module.F90 to become negative.
Then on line 2537 log(term) is evaluated, which causes the algorithm to exit prematurely.

muller failure

Muller has a failed test case when using quad precision

fpm test --compiler ifort  --flag "-DREAL128"

It claims to have succeeded, but doesn't produce a valid root:

              method fun    n            error                         x                f evals iflag runs/sec
-------------------- --- ---- ---------------- ------------------------- ---------------- ----- ----- --------
              muller   2    1     2.270847E-01    3.2499999995000000E+00     3.192712E+00     5     0  2.6E+03

The actual root is at x=3.0229153472730570E+00.

Passing external parameters to functions

Hi,
Thanks for making these codes available.
I'm trying to find a way to pass additional parameters (beisdes the argument) to the objective function, eg instead of f(x) have f(x, parameters), where "parameters" could be an ad-hoc structure with approriate number of elements (eg parameters%alpha which is set in the main program).
Is there a quick way to do so in you root_module?
Thanks
Gianni

Vectorized interface

It could be nice to support a vector-instruction-friendly interface. There is an open issue for this in scipy/scipy#7242. The purpose is to solve a family of problems:

$$f(x_k,p_k) = 0, \qquad k = 1, ..., N$$

Ideally, the $p_k$ are somehow related in their physical origin (e.g. a spatial field), so the convergence behavior locally in terms of $k$ will be similar. For root-finding on multi-dimensional parameter grids, e.g. $p_{j,k}$, one can use the Fortran pointer-remapping feature to make the problems 1-D.

I did some benchmarking of zeroin recently (discussion here). The code runs in scalar mode for the most part. The complex control flow prevents vectorization (view on godbolt; code taken from netlib):

$ gfortran-13 -c -O3 -march=haswell -fopt-info-missed zeroin.f
zeroin.f:124:72: missed:   not inlinable: zeroin/0 -> __builtin_copysign/1, function body not available
zeroin.f:60:72: missed: couldn't vectorize loop
zeroin.f:60:72: missed: not vectorized: control flow in loop.
zeroin.f:62:10: missed: couldn't vectorize loop
zeroin.f:62:10: missed: not vectorized: control flow in loop.
zeroin.f:125:72: missed: statement clobbers memory: fb_113 = f_86(D) (&b);
zeroin.f:53:72: missed: statement clobbers memory: fa_88 = f_86(D) (&a);
zeroin.f:54:72: missed: statement clobbers memory: fb_90 = f_86(D) (&b);
zeroin.f:125:72: missed: statement clobbers memory: fb_113 = f_86(D) (&b);

One may need to write the implementation in terms of "chunks" with reductions, which would then map to SIMD registers.

I'm not sure how the vector callback interface would look like, and what is the natural way to pass the parameters in a way that makes it SIMD-friendly:

integer, parameter :: vlen = 4  ! or 8 (depending on real kind and instruction set)

! Explicit length
function vf(x,k)
   real, intent(in) :: x
   integer, intent(in) :: k
   real :: vf(vlen)

   ! using loop
   !$omp simd
   do j = 1, vlen
      vf(j) = ... ! F(x,p(k + j)), p imported from host scope
   end do

   ! or array expressions
   vf = ... ! F(x,p(k:k+vlen)), p imported from host scope
end function

! Scalar callback vectorized using OpenMP
function f(x,k)
!$omp declare simd uniform(x) linear(k: 1)
  real, intent(in) :: x
  integer, intent(in) :: k   ! index i needed to index into parameter array
  real :: f
  f = ... ! F(x,p(k)), p imported from host scope
end function

I suppose you could also do a callback of the form:

subroutine root_callback(x,lb,ub,y)
real, intent(in) :: x
integer, intent(in) :: lb, ub  ! indexes needed to reference into parameter array
real, intent(out) :: y(lb:ub)

! user writes the loop
!$omp simd
do k = lb, ub
  y(k) = ... ! F(x,p(k)), p imported from host scope
end do

! or using array expressions
associate (p => params(lb:ub))
y = ... ! F(x,p), params imported from host scope
end associate

end subroutine

This way the SIMD length could be left to the program logic, and it would make it easier to handle peel/remainder loops. A more Fortranic way would be to use do concurrent (assuming compilers will do the right thing).

Termination conditions

Is there any interest to have custom termination conditions? This could be implemented with a user-provided callback (procedure pointer). If nothing else it could be useful for comparison with other root-finders in terms of speed or number of function evaluations needed for a given accuracy.

Below is a summary of what the other libraries have (in addition to the classic maximum iterations criterion).


Boost

The default condition used by Boost is

when the relative distance between a and b is less than four times the machine epsilon for T, or 2^{1-bits}, whichever is the larger. In other words, you set bits to the number of bits of precision you want in the result. The minimal tolerance of four times the machine epsilon of type T is required to ensure that we get back a bracketing interval, since this must clearly be at greater than one epsilon in size. While in theory a maximum distance of twice machine epsilon is possible to achieve, in practice this results in a great deal of "thrashing" given that the function whose root is being found can only ever be accurate to 1 epsilon at best.

To get "maximum" precision you would do something like this:

  int digits = std::numeric_limits<T>::digits;  // Maximum possible binary digits accuracy for type T.
  // Some fraction of digits is used to control how accurate to try to make the result.
  int get_digits = digits - 3;                  // We have to have a non-zero interval at each step, so
                                                // maximum accuracy is digits - 1.  But we also have to
                                                // allow for inaccuracy in f(x), otherwise the last few
                                                // iterations just thrash around.

In Fortran the equivalent would be

real(dp) :: x, f
integer :: get_digits
get_digits = digits(x) - 3

Boost also gives the possibility to specify a custom termination functor for the root bracket:

template <class T>
struct eps_custom
{
   eps_custom();
   bool operator()(const T& a, const T& b) const; // user-defined
};

It provides also a few predefined termination condition functors to stop at the nearest integer of the true root, or at the integer ceiling/floor of the true root. I don't really know what is the usage case for these.

SciPy

The methods in Scipy generally use a combination of absolute and relative tolerance with small variations picking the factor multiplying the relative tolerance:

  • Bisection
    dm = xb - xa;
 // ...
        dm *= .5;
        if (fm == 0 || fabs(dm) < xtol + rtol*fabs(xm)) {
            solver_stats->error_num = CONVERGED;
            return xm;
        }
  • Brent's method (same as bisection other just multiplied by 1/2 on both sides)
        delta = (xtol + rtol*fabs(xcur))/2;
        sbis = (xblk - xcur)/2;
        if (fcur == 0 || fabs(sbis) < delta) {
            solver_stats->error_num = CONVERGED;
            return xcur;
        }
  • Ridder's method (uses full interval instead of half the interval, tolerance calculated with new estimate)
        tol = xtol + rtol*xn;
        if (fn == 0.0 || fabs(xb - xa) < tol) {
            solver_stats->error_num = CONVERGED;
            return xn;
        }
  • TOMS748 (uses np.isclose(a,b) which is defined as absolute(a - b) <= (atol + rtol * absolute(b)))
        a, b = self.ab[:2]
        if np.isclose(a, b, rtol=self.rtol, atol=self.xtol):
            return _ECONVERGED, sum(self.ab) / 2.0

Roots.jl

For most algorithms, convergence is decided when

The value |f(x_n)| <= tol with tol = max(atol, abs(x_n)*rtol), or

the values x_n ≈ x_{n-1} with tolerances xatol and xrtolandf(x_n) ≈ 0with a _relaxed_ tolerance based onatolandrtol`.

GSL

GSL provides three search stopping criteria:

  • gsl_root_test_interval - $| a - b| &lt; epsabs + epsrel \min(|a|,|b|)$, with some special handling when $[a,b]$ contains the origin
  • gsl_root_test_delta - $|x_1 - x_0| &lt; \hbox{\it epsabs} + \hbox{\it epsrel}\,|x_1|$
  • gsl_root_test_residual - $|f| &lt; epsabs$

Since the GSL solver uses a reverse-communication interface, a user can easily supply his own stopping condition too.

Are polynomial roots in scope?

In the spirit of question #4, are root finders for polynomials (both real and complex) also in scope?

Examples from various other languages:

Other resources:

An assortment of methods (predominantly implemented in Fortran) can be found under the GAMS Class F1a. The most famous of these are probably the subroutines RPOLY and CPOLY from Jenkins and Traub (1975) which can be found on Netlib. Newer methods have also been written in Fortran, including qralg.f (part of /opt/companion.tgz on Netlib) from Edelman & Murakami (1995), the Codes from Skowron & Gould (2012) and eiscor - eigensolvers based on unitary core transformations containing the AMVW method from the work of Aurentz et al. (2015), Fast and Backward Stable Computation of Roots of Polynomials (an earlier version can be picked up from the website of Ran Vandebril, one of the co-authors of that paper). The Julia packages are actually ports of these Fortran codes.

I demonstrated a solver using the companion matrix and LAPACK on Discourse a while ago, but I understand that more specialized methods perform better in terms of accuracy and speed for ill-conditioned or large problems. The blog post from Cleve Moler at Cleve's Corner discusses his decision to use the companion matrix approach for MATLAB's roots.

Personally, I think a separate package might be more suitable as tests for polynomials roots would require different infrastructure, and in general the solvers might require more linear-algebra machinery.

Multi-dimensional root-finding

Again, I don't know if this is in scope with the goal of the project, but it would be nice to have a similar interface to solve also systems of nonlinear equations (Newton-Raphson, Broyden's, Powell's methods, etc...).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.