defmodule Chi2fit.Math do
# Copyright 2016-2021 Pieter Rijken
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
@typedoc "Supported numerical integration methods"
@type method :: :gauss | :gauss2 | :gauss3 | :romberg | :romberg2 | :romberg3
@doc """
Calculates the partial derivative of a function and returns the value.
## Examples
The function value at a point:
iex> der([3.0], fn [x]-> x*x end) |> Float.round(3)
9.0
The first derivative of a function at a point:
iex> der([{3.0,1}], fn [x]-> x*x end) |> Float.round(3)
6.0
The second derivative of a function at a point:
iex> der([{3.0,2}], fn [x]-> x*x end) |> Float.round(3)
2.0
Partial derivatives with respect to two variables:
iex> der([{2.0,1},{3.0,1}], fn [x,y] -> 3*x*x*y end) |> Float.round(3)
12.0
"""
@default_h 0.001
@spec der([float|{float,integer}], (([float])->float), Keyword.t) :: float
def der(parameters, fun, options \\ []) do
richardson(fn acc ->
result = parameters
|> expand_pars(acc)
|> reduce_pars
|> Enum.reduce(0.0, fn ({x,n,dx},sum) when is_list(x) -> sum+n*fun.(x)/dx end)
{result,acc/2.0}
end,
@default_h,4.0,options)
end
@doc """
Calculates the jacobian of the function at the point `x`.
## Examples
iex> jacobian([2.0,3.0], fn [x,y] -> x*y end) |> Enum.map(&Float.round(&1))
[3.0, 2.0]
"""
@spec jacobian(x :: [float], (([float])->float)) :: [float]
def jacobian(x, fun, options \\ []) do
jacfun = &(jacobian(x, &1, fun, options))
Enum.reduce(length(x)..1, [], fn (k,acc) -> [jacfun.(k)|acc] end)
end
## TODO: implement gauss-kronrad integration (progressive gauss)
@doc """
Numerical integration providing Gauss and Romberg types.
"""
@default_points 32
@spec integrate(method, ((float)->float), a::float, b::float, options::Keyword.t) :: float
def integrate(method, func, a, b, options \\ [])
def integrate(:gauss, func, a, b, options) do
npoints = options[:points] || @default_points
factor_min = (b-a)/2.0
factor_plus = (b+a)/2.0
{weights,abscissa} = case npoints do
4 ->
{
[ 0.6521451548625461,0.3478548451374538 ],
[ 0.3399810435848563,0.8611363115940526 ]
}
8 ->
{
[ 0.3626837833783620,0.3137066458778873,0.2223810344533745,0.1012285362903763 ],
[ 0.1834346424956498,0.5255324099163290,0.7966664774136267,0.9602898564975363 ]
}
32 ->
{
[ 0.0965400885147278,0.0956387200792749,0.0938443990808046,0.0911738786957639,0.0876520930044038,0.0833119242269467,0.0781938957870703,0.0723457941088485,0.0658222227763618,0.0586840934785355,0.0509980592623762,0.0428358980222267,0.0342738629130214,0.0253920653092621,0.0162743947309057,0.0070186100094701 ],
[ 0.0483076656877383,0.1444719615827965,0.2392873622521371,0.3318686022821277,0.4213512761306353,0.5068999089322294,0.5877157572407623,0.6630442669302152,0.7321821187402897,0.7944837959679424,0.8493676137325700,0.8963211557660521,0.9349060759377397,0.9647622555875064,0.9856115115452684,0.9972638618494816 ]
}
end
factor_min * (Enum.zip(abscissa,weights) |> Enum.map(fn {x,w} -> w*( func.(factor_min*x+factor_plus) + func.(-factor_min*x+factor_plus) ) end) |> Enum.sum)
end
def integrate(:gauss2, func, a, :infinity, options) do
fac = 500.0 ## t = tanh(x/fac)
fac*integrate(:gauss, fn t -> (func.(fac*:math.atanh(t)))/(1.0-t*t) end, :math.tanh(a/fac), 1.0, options)
end
def integrate(:gauss2, func, a, b, options) do
fac = 500.0 ## t = tanh(x/fac)
fac*integrate(:gauss, fn t -> (func.(fac*:math.atanh(t)))/(1.0-t*t) end, :math.tanh(a/fac), :math.tanh(b/fac), options)
end
def integrate(:gauss3, func, a, :infinity, options) do
## x = t/(1-t) = -1 + 1/(1-t), dx = dt/(1-t)^2
integrate(:gauss, fn t -> (func.(t/(1.0-t)))/(1.0-t)/(1.0-t) end, a/(a+1.0), 1.0, options)
end
def integrate(:gauss3, func, a, b, options) do
## x = t/(1-t) = -1 + 1/(1-t), dx = dt/(1-t)^2
integrate(:gauss, fn t -> (func.(t/(1.0-t)))/(1.0-t)/(1.0-t) end, a/(a+1.0), b/(b+1.0), options)
end
@default_tolerance 1.0e-6
def integrate(:romberg, func, a, b, options) do
richardson(fn acc ->
case acc do
[] ->
f1 = try do func.(a) rescue _e -> 0.0 end
f2 = try do func.(b) rescue _e -> 0.0 end
result = (b-a) * ( f1 + f2 )/2.0
{result,[{a,f1},{b,f2}]}
values ->
vals = values
|> Stream.transform(nil, fn
{x2,f},nil -> {[{x2,f}],x2}
{x2,f},x1 -> {[{(x2+x1)/2.0,func.((x2+x1)/2.0)},{x2,f}],x2}
end)
|> Enum.to_list
result = vals
|> Stream.chunk_every(2,1,:discard)
|> Stream.map(fn [{x1,f1},{x2,f2}] -> (x2-x1)*( f1 + f2 )/2.0 end)
|> Enum.sum
{result,vals}
end
end, [], 4.0, options)
end
def integrate(:romberg2, func, a, :infinity, options) do
fac = 500.0 ## t = tanh(x/fac)
integrate(:romberg, fn t -> (func.(fac*:math.atanh(t)))*fac/(1.0-t*t) end, :math.tanh(a/fac), 1.0, options)
end
def integrate(:romberg2, func, a, b, options) do
fac = 500.0 ## t = tanh(x/fac)
integrate(:romberg, fn t -> (func.(fac*:math.atanh(t)))*fac/(1.0-t*t) end, :math.tanh(a/fac), :math.tanh(b/fac), options)
end
def integrate(:romberg3, func, a, :infinity, options) do
## x = t/(1-t) = -1 + 1/(1-t), dx = dt/(1-t)^2
integrate(:romberg, fn t -> (func.(t/(1.0-t)))/(1.0-t)/(1.0-t) end, a/(a+1.0), 1.0, options)
end
def integrate(:romberg3, func, a, b, options) do
## x = t/(1-t) = -1 + 1/(1-t), dx = dt/(1-t)^2
integrate(:romberg, fn t -> (func.(t/(1.0-t)))/(1.0-t)/(1.0-t) end, a/(a+1.0), b/(b+1.0), options)
end
@doc """
Richardson extrapolation.
"""
@default_tolerance 1.0e-6
@spec richardson(func::((term)->{float,term}), init::term, factor::float, results::[float], options::Keyword.t) :: float
def richardson(func, init, factor, results \\ [], options)
def richardson(func, init, factor, results, options) do
tolerance = options[:tolerance] || @default_tolerance
max = options[:itermax]
{result,acc} = func.(init)
{new,last,error,_} = results |> Enum.reduce({[],result,nil,factor}, fn
_prev,{acc,item,0.0,order} ->
{acc,item,0.0,order}
prev,{acc,item,_,order} ->
diff = (order*item - prev)/(order-1.0)
{[diff|acc],diff,if(diff==0, do: 0.0, else: abs((diff-item)/diff)),order*factor}
end)
cond do
max && (length(new) > max) ->
last
error < tolerance ->
last
true ->
richardson(func, acc, factor, [result|Enum.reverse(new)], options)
end
end
@doc """
Newton-Fourier method for locating roots and returning the interval where the root is located.
See [https://en.wikipedia.org/wiki/Newton%27s_method#Newton.E2.80.93Fourier_method]
"""
@spec newton(a::float,b::float,func::((x::float)->float),maxiter::non_neg_integer,options::Keyword.t) :: {float, {float,float}, {float,float}}
def newton(a,b,func,maxiter \\ 10, options), do: newton(a,b,func,maxiter,{(a+b)/2,{a,b},{nil,nil}},options)
##
## Local functions
##
defp jacobian(x=[_|_], k, fun, options) when k>0 and k<=length(x) and is_function(fun,1) do
x |> List.update_at(k-1, fn (val) -> {val,1} end) |> der(fun,options)
end
@default_rel_tolerance 1.0e-6
defp newton(_a,_b,func,0,{root,{l,r},_},_options), do: {root,{l,r},{func.(l),func.(r)}}
defp newton(a,b,func,maxiter,{prev,{left,right},{vleft,vright}},options) do
tolerance = options[:tolerance] || @default_rel_tolerance
x0 = func.(right)
z0 = func.(left)
if x0*z0 > 0 do
raise ArgumentError, message: "Interval does not contain root"
end
derx0 = der([{right,1}], fn [x]->func.(x) end, options)
if derx0 == 0 do
raise ArithmeticError,
message: "Interval contains local minimum/maximum [left/z0=#{left}/#{z0}; right/x0=#{right}/#{x0}; der=#{derx0}]"
end
x1 = right - x0/derx0
z1 = left - z0/derx0
root = (x1+z1)/2.0
cond do
z1 < left ->
newton(a,b,func,0,{prev,{left,right},{vleft,vright}},options)
x1 > right ->
newton(a,b,func,0,{prev,{left,right},{vleft,vright}},options)
z1 < x1 and abs(x1-z1) < tolerance ->
newton(a,b,func,0,{root,{z1,x1},{z0,x0}},options)
z1 > x1 and abs(x1-z1) < tolerance ->
newton(a,b,func,0,{root,{x1,z1},{z0,x0}},options)
z1 > x1 ->
newton(a,b,func,maxiter-1,{prev,{x1,z1},{z0,x0}},options)
true ->
newton(a,b,func,maxiter-1,{root,{z1,x1},{z0,x0}},options)
end
end
defp reduce_pars(list) do
list |> Enum.reduce([{[],1,1.0}],
fn
(list,acc) when is_list(list) ->
Enum.flat_map(list,
fn
({{x,0,dx1}}) -> Enum.map(acc, fn ({y,n,dx2})->{[x|y],-n,dx1*dx2} end)
({x,0,dx1}) -> Enum.map(acc, fn ({y,n,dx2})->{[x|y],n,dx1*dx2} end)
end)
({x,0,dx1},acc) -> Enum.map(acc, fn ({y,n,dx2})->{[x|y],n,dx1*dx2} end)
end)
|> Enum.map(fn ({l,n,dx}) -> {Enum.reverse(l),n,dx} end)
end
defp expand_pars(list,h) do
list |> Enum.map(
fn
({{x,0,factor}}) -> {{x,0,factor}}
({{x,0}}) -> {{x,0,1.0}}
({{x,n,factor}}) when n>0 ->
xplus = x*(1.0+h)
xmin = x*(1.0-h)
dx = xplus-xmin
[{{xplus,n-1,factor*dx}},{xmin,n-1,factor*dx}] |> expand_pars(h) |> List.flatten
({{x,n}}) when n>0 ->
xplus = x*(1.0+h)
xmin = x*(1.0-h)
dx = xplus-xmin
[{{xplus,n-1,dx}},{xmin,n-1,dx}] |> expand_pars(h) |> List.flatten
({x,0,factor}) -> {x,0,factor}
({x,0}) -> {x,0,1.0}
({x,n,factor}) when n>0 ->
xplus = x*(1.0+h)
xmin = x*(1.0-h)
dx = xplus-xmin
[{xplus,n-1,factor*dx},{{xmin,n-1,factor*dx}}] |> expand_pars(h) |> List.flatten
({x,n}) when n>0 ->
xplus = x*(1.0+h)
xmin = x*(1.0-h)
dx = xplus-xmin
[{xplus,n-1,dx},{{xmin,n-1,dx}}] |> expand_pars(h) |> List.flatten
(x) when is_number(x) -> {x,0,1.0}
end)
end
end