lib/distributions/wishart.ex

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

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

end


defimpl Chi2fit.Distribution, for: Chi2fit.Distribution.Wishart do
  alias Chi2fit.Distribution, as: D
  alias Chi2fit.Matrix, as: M
  
  import D.Wishart
  alias D.Wishart

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

  defp makerow([_],_,_pars,row), do: Enum.reverse(row)
  defp makerow([{_,_,_,rprev},{i,p,g,r}|pgrrest],[{_,q}|qrest],{a,pi,gi},row) do
    a = a - pi*rprev + 2*q/gi/g
    makerow([{i,p,g,r}|pgrrest],qrest,{a,pi,gi},[a|row])
  end
  
  defp makemat(pgrlist, qlist), do: makemat(pgrlist,qlist,[])
  defp makemat([], _, mat), do: Enum.reverse(mat)
  defp makemat(pgrlist, qlist, mat) do
    [{i,p,g,_}|pgrrest] = pgrlist
    makemat(pgrrest,tl(qlist),[makerow(pgrlist,qlist,{0,p,g},List.duplicate(0,i))|mat])
  end

  defp gamma(m,x) do
    :math.pow(:math.pi(),m*(m-1)/4)*Enum.reduce(1..m, 1.0, fn i,acc -> acc*tgamma(x-(i-1)/2) end)
  end
  
  @spec wishartCDF(number,number,number,number) :: (number -> number)
  defp wishartCDF(mu,scale,m,n) when is_number(mu) and is_number(scale) and scale>0 do
    nmin = min(m,n)
    nmax = max(m,n)

    alpha = (nmax - nmin - 1)/2
        
    k = :math.pow(:math.pi(),nmin*nmin/2)/:math.pow(2,nmin*nmax/2)/gamma(nmin,nmax/2)/gamma(nmin,nmin/2)
    
    nmat = if rem(nmin,2) == 0, do: nmin, else: nmin + 1
    kprime = k*:math.pow(2,alpha*nmat + nmat*(nmat+1)/2)*Enum.reduce(1..nmat, 1, fn i,acc -> acc*tgamma(alpha+i) end)
    
    fn
      x when x == mu -> 0.0
      x when x < mu -> 0.0
      x when x > mu ->
        xprime = (x-mu)/scale
        
        pgrlist = 1..nmin |> Enum.map(& {&1, gamma_p(alpha+&1, xprime/2), tgamma(alpha+&1), :math.exp(-xprime/2)*:math.pow(xprime/2,alpha+&1)/tgamma(alpha+1+&1)})
        qlist = 2..2*nmin-1 |> Enum.map(& {&1, :math.pow(2, -2*alpha-&1)*tgamma(2*alpha+&1)*gamma_p(2*alpha+&1,xprime)})
                
        mata = makemat(pgrlist,qlist)
        matt = M.transpose(mata)
        M.subtract(mata, matt) |> M.det |> :math.sqrt |> then(& kprime*&1)
    end
  end
  defp wishartCDF(_mu,_scale,_m,_n), do: raise ArithmeticError, "Wishart is only defined for positive scale"

  def skewness(%Wishart{}), do: raise ArithmeticError, "Skewness not supported for Wishart distribution"
  def kurtosis(%Wishart{}), do: raise ArithmeticError, "Kurtosis not supported for Wishart distribution"
  def pdf(%Wishart{pars: nil}), do: raise ArithmeticError, "pdf not supported for Wishart distribution"
  def random(%Wishart{pars: nil}), do: raise ArithmeticError, "random not supported for Wishart distribution"

  def size(%Wishart{pars: nil}), do: 2

  def cdf(%Wishart{pars: nil, dim: [m,n]}), do: fn x,[mu,scale] -> wishartCDF(mu,scale,m,n).(x) end

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

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

end