lib/distributions/weibull.ex

defmodule Chi2fit.Distribution.Weibull 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 """
  Weibull distribution.
  """

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

end

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

  import D.Weibull
  alias D.Weibull

  import Exboost.Math, only: [tgamma: 1]

  @gamma53 0.902745292950933611297
  @gamma32 0.886226925452758013649

  @spec weibull(number, number|Keyword.t) :: ((...) -> number)
  defp weibull(1.0, [avg: average]), do: weibull(1.0, average)
  defp weibull(1.5, [avg: average]), do: weibull(1.5, average/@gamma53)
  defp weibull(2.0, [avg: average]), do: weibull(2.0, average/@gamma32)
  defp weibull(alpha, beta) when is_number(alpha) and is_number(beta) do
    fn ->
      u = :rand.uniform()
      beta*:math.pow(-:math.log(u),1.0/alpha)
    end
  end
  
  @spec weibullCDF(number,number) :: (number -> number)
  defp weibullCDF(k,_) when k<0, do: raise ArithmeticError, "Weibull is only defined for positive shape"
  defp weibullCDF(_,lambda) when lambda<0, do: raise ArithmeticError, "Weibull is only defined for positive scale"
  defp weibullCDF(k,lambda) when is_number(k) and is_number(lambda) do
    fn
      0 -> 0.0
      0.0 -> 0.0
      x when x<0 -> 0.0
      x ->
        lg = :math.log(x/lambda)*k
        cond do
          lg > 100.0 ->
            0.0
          lg < -18.0 ->
            ## With -18 (x/lambda)^2k < 10^(-16)
            t = :math.pow(x/lambda,k)
            t*(1 - 0.5*t)
          true ->
            1.0 - :math.exp(-:math.pow(x/lambda,k))
        end
    end
  end

  @spec weibullPDF(number,number) :: (number -> number)
  defp weibullPDF(k,_) when k<0, do: raise ArithmeticError, "Weibull is only defined for positive shape"
  defp weibullPDF(_,lambda) when lambda<0, do: raise ArithmeticError, "Weibull is only defined for positive scale"
  defp weibullPDF(k,lambda) when is_number(k) and is_number(lambda) do
    fn
      0 -> 0.0
      0.0 -> 0.0
      x when x<0 -> 0.0
      x ->
        t = :math.pow(x/lambda, k)
        k*t/x*:math.exp( -t )
    end
  end
  
  def skewness(%Weibull{pars: nil}) do
    fn [k,lambda] ->
      mu = lambda*tgamma(1+1/k)
      sigma = lambda*:math.sqrt(tgamma(1+2/k) - tgamma(1+1/k)*tgamma(1+1/k))
      tgamma(1+3/k)*:math.pow(lambda/sigma,3) - 3*mu/sigma - :math.pow(mu/sigma,3)
    end
  end
  def kurtosis(%Weibull{pars: nil}) do
    fn [k,lambda] ->
      mu = lambda*tgamma(1+1/k)
      sigma = lambda*:math.sqrt(tgamma(1+2/k) - tgamma(1+1/k)*tgamma(1+1/k))
      skew = tgamma(1+3/k)*:math.pow(lambda/sigma,3) - 3*mu/sigma - :math.pow(mu/sigma,3)
      tgamma(1+4/k)*:math.pow(lambda/sigma,4) - 4*mu/sigma*skew - 6*:math.pow(mu/sigma,2) - :math.pow(mu/sigma,4) - 3.0
    end
  end
  def size(%Weibull{pars: nil}), do: 2
  def size(%Weibull{pars: [k,nil]}) when is_number(k), do: 1
  def size(%Weibull{pars: [nil,lambda]}) when is_number(lambda), do: 1
  def size(%Weibull{pars: [k,lambda]}) when is_number(k) and is_number(lambda), do: 0

  def cdf(%Weibull{pars: nil}), do: fn x,[k,lambda] -> weibullCDF(k,lambda).(x) end
  def cdf(%Weibull{pars: [k,nil]}), do: fn x,[lambda] -> weibullCDF(k,lambda).(x) end
  def cdf(%Weibull{pars: [nil,lambda]}), do: fn x,[k] -> weibullCDF(k,lambda).(x) end
  def cdf(%Weibull{pars: [k,lambda]}), do: fn x -> weibullCDF(k,lambda).(x) end

  def pdf(%Weibull{pars: nil}), do: fn x, [k,lambda] -> weibullPDF(k,lambda).(x) end
  def pdf(%Weibull{pars: [k,nil]}), do: fn x, [lambda] -> weibullPDF(k,lambda).(x) end
  def pdf(%Weibull{pars: [nil,lambda]}), do: fn x, [k] -> weibullPDF(k,lambda).(x) end
  def pdf(%Weibull{pars: [k,lambda]}), do: fn x -> weibullPDF(k,lambda).(x) end

  def random(%Weibull{pars: [k,lambda]}), do: weibull(k,lambda).()
  def random(%Weibull{pars: nil}), do: fn [k,lambda] -> weibull(k,lambda).() end

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

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

end