lib/distributions/sep.ex

defmodule Chi2fit.Distribution.SEP do

  # Copyright 2019 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.

  @moduledoc """
  The Skew Exponential Power cumulative distribution (Azzalini).

  ## Options
      `:method` - the integration method to use, :gauss and :romberg types are supported, see below
      `:tolerance` - re-iterate until the tolerance is reached (only for :romberg)
      `:points` - the number of points to use in :gauss method

  ## Integration methods
      `:gauss` - n-point Gauss rule,
      `:gauss2` - n-point Guass rule with tanh transformation,
      `:gauss3` - n-point Gauss rule with linear transformstion,
      `:romberg` - Romberg integration,
      `:romberg2` - Romberg integration with tanh transformation,
      `:romberg3` - Romberg integration with linear transformstion.
  """

  defstruct [:pars, :offset, options: [], name: "sep"]
  
  @type t() :: %__MODULE__{
    pars: [number()] | nil,
    offset: number() | nil,
    options: Keyword.t,
    name: String.t
  }

end

defimpl Chi2fit.Distribution, for: Chi2fit.Distribution.SEP do
  alias Chi2fit.Distribution, as: D

  import D.SEP
  alias D.SEP

  import Chi2fit.Math, only: [integrate: 5]

  @pi :math.pi()
  
  @spec sepCDF(a :: float,b :: float,lambda :: float,alpha :: float, options :: Keyword.t) :: (number -> number)
  defp sepCDF(a,b,lambda,alpha,options) do
    method = options[:method] || :romberg2
    endpoint = if method in [:gauss2,:gauss3,:romberg2,:romberg3], do: :infinity, else: 1000.0
    fn
      x ->
        result2 = integrate(method, sepPDF(a,b,lambda,alpha), 0.0, x, options)
        result3 = integrate(method, sepPDF(a,b,lambda,alpha), 0.0, endpoint, options)
        result2/result3
    end
  end

  @spec sepPDF(a::float,b::float,lambda::float,alpha::float) :: (number -> number)
  defp sepPDF(a,b,lambda,alpha) do
    fn x ->
      z = (x-a)/b
      t = :math.pow(abs(z),alpha/2.0)
      w = lambda*:math.sqrt(2.0/alpha)*t

      if z > 0.0 do
        :math.exp(-t*t/alpha) * 0.5 * ( 1.0 + :math.erf(w/:math.sqrt(2.0)) )
      else
        :math.exp(-t*t/alpha) * 0.5 * ( :math.erfc(w/:math.sqrt(2.0)) )
      end
    end
  end

  def skewness(%SEP{pars: nil}) do
    fn [_a,_b,lambda,_alpha] ->
      delta = lambda/:math.sqrt(1+lambda*lambda)
      0.5*(4-@pi)*:math.pow(delta*:math.sqrt(2/@pi),3)/:math.pow(1-2*delta*delta/@pi,1.5)
    end
  end
  def kurtosis(%SEP{pars: nil}) do
    fn [_a,_b,lambda,_alpha] ->
        delta = lambda/:math.sqrt(1+lambda*lambda)
        2*(@pi-3)*:math.pow(delta*:math.sqrt(2/@pi),4)/:math.pow(1-2*delta*delta/@pi,2)
    end
  end

  def size(%SEP{offset: nil}), do: 4
  def size(%SEP{offset: offset}) when is_number(offset), do: 3
  
  def cdf(%SEP{pars: nil, options: options}), do: fn x,[a,b,lambda,alpha] -> sepCDF(a,b,lambda,alpha,options).(x) end

  def pdf(%SEP{pars: nil}), do: fn x,[a,b,lambda,alpha] -> sepPDF(a,b,lambda,alpha).(x) end
  def random(%SEP{}), do: raise D.FunctionNotSupportedError, message: "random is not supported for the SEP distribution"

  def name(model), do: model.name
  
end

defimpl Inspect, for: Chi2fit.Distribution.SEP do
  import Inspect.Algebra
  
  def inspect(dict, opts) do
    case {dict.pars,dict.offset} do
      {nil,nil} ->
        "#SEP<>"
      {nil,offset} ->
        concat ["#SEP<", to_doc("offset=#{offset}", opts), ">"]
      {[scale,lambda,alpha],nil} ->
        concat ["#SEP<", to_doc("scale=#{scale}, lambda=#{lambda}, alpha=#{alpha}", opts), ">"]
      {[scale,lambda,alpha],offset} ->
        concat ["#SEP<", to_doc("offset=#{offset}, scale=#{scale}, lambda=#{lambda}, alpha=#{alpha}", opts), ">"]
      {list,offset} ->
        concat ["#SEP<", "offset=#{offset}, ", to_doc(list, opts), ">"]
    end
  end

end