All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.jonnylaw.model.Model.scala Maven / Gradle / Ivy

There is a newer version: 0.6.0
Show newest version
package com.github.jonnylaw.model

import breeze.numerics.{cos, sin, sqrt, exp, log}
import breeze.stats.distributions._
import breeze.linalg.{DenseMatrix, DenseVector}

import cats._
import cats.Semigroup
import cats.implicits._
import cats.data.Reader

import StateSpace._

trait Model { self =>
  /**
    * The observation model, a function from eta to a distribution over the observations
    * realisations can be produced from the observation model by calling draw
    */
  def observation: Eta => Rand[Observation]
  /**
    * The linking-function, transforms the state space into the parameter space of the 
    * observation distribution using a possibly non-linear transformation
    */
  def link(x: Gamma): Eta = Vector(x)
  /**
    * The Linear, deterministic transformation function. f is used to add seasonal factors or
    * other time depending linear transformations
    */ 
  def f(s: State, t: Time): Gamma
  /**
    * An exact or approximate solution to a diffusion process, used to advance the latent state.
    * This function returns a distribution over the next state and can be simulated from
    */
  def sde: Sde
  /**
    * The data likelihood, given a fully transformed latent state, eta, and an observation
    * the log-likelihood can be calculated for use in inference algorithms
    */
  def dataLikelihood: (Eta, Observation) => LogLikelihood

}

object Model {
  def poissonModel(sde: SdeParameter => Sde): Reader[Parameters, Model] = Reader { p => p match {
    case param: LeafParameter => PoissonModel(sde(param.sdeParam), param)
    case _ => throw new Exception("Can't build model from branch parameter")
  }}

  def seasonalModel(
    period: Int,
    harmonics: Int,
    sde: SdeParameter => Sde): Reader[Parameters, Model] = Reader { p => p match {
      case param: LeafParameter => SeasonalModel(period, harmonics, sde(param.sdeParam), param)
      case _ => throw new Exception("Can't build model from branch parameter")
  }}

  def linearModel(sde: SdeParameter => Sde): Reader[Parameters, Model] = Reader { p => p match {
    case param: LeafParameter => LinearModel(sde(param.sdeParam), param)
    case _ => throw new Exception("Can't build model from branch parameter")
  }}

  def studentsTModel(sde: SdeParameter => Sde, df: Int): Reader[Parameters, Model] = Reader { p => p match {
    case param: LeafParameter => StudentsTModel(sde(param.sdeParam), df, param)
    case _ => throw new Exception("Can't build model from branch parameter")
  }}

  def bernoulliModel(sde: SdeParameter => Sde): Reader[Parameters, Model] = Reader { p => p match {
    case param: LeafParameter => BernoulliModel(sde(param.sdeParam), param)
    case _ => throw new Exception("Can't build model from branch parameter")
  }}

  def lgcpModel(sde: SdeParameter => Sde): Reader[Parameters, Model] = Reader { p => p match {
    case param: LeafParameter => LogGaussianCox(sde(param.sdeParam), param)
    case _ => throw new Exception("Can't build model from branch parameter")
  }}

  /**
    * Models form a semigroup, they can be combined to form a composed model
    */
  implicit def modelSemigroup = new Semigroup[UnparamModel] {
    def combine(m1: UnparamModel, m2: UnparamModel): UnparamModel =
      compose(m1, m2)
  }

  /**
    * Combine two unparameterised models, usually applied with infix notation |+|
    * by importing cats.implicits._, this is not commutative, the observation distribution must 
    * be on the left-hand side of the composition
    * @param mod1 the left-hand model in the composition, if this is a composition of two
    * then the model with the desired observation distribution must be mod1
    * @param mod2 the right-hand model in the composition
    * @return a composed model of mod1 and mod2, which can be composed again
    */
  def compose(mod1: UnparamModel, mod2: UnparamModel): UnparamModel = Reader { p => p match {
    case BranchParameter(lp, rp) => {
      new Model {
        def observation = x => mod1(lp).observation(x)

        override def link(x: Double) = mod1(lp).link(x)

        def f(s: State, t: Time) = s match {
          case Branch(ls, rs) =>
            mod1(lp).f(ls, t) + mod2(rp).f(rs, t)
          case x: Leaf[DenseVector[Double]] =>
            mod1(lp).f(x, t)
        }

        def sde: Sde = mod1(lp).sde |+| mod2(rp).sde

        def dataLikelihood = (s, y) => mod1(lp).dataLikelihood(s, y)
      }
    }
    case _ => throw new Exception("Can't Build composed model from leafParameter")
  }}
}

/**
  * Generalised student t model
  * @param stepFun the diffusion process solution to use for this model
  * @return an model of the Student-T model, which can be composed with other models
  */

private final case class StudentsTModel(sde: Sde, df: Int, p: LeafParameter) extends Model {
  def observation = x => p.scale match {
    case Some(v) => StudentsT(df) map (a => a * v + x.head)
    case None => throw new Exception("No scale parameter provided to Student T Model")
  }

  def f(s: State, t: Time) = s.fold(0.0)((x: DenseVector[Double]) => x(0))(_ + _)


  def dataLikelihood = (eta, y) => p.scale match {
    case Some(v) => 1/v * StudentsT(df).logPdf((y - eta.head) / v)
    case None => throw new Exception("No scale parameter provided to Student T Model")
  }
}

  /**
    * A seasonal model
    * @param period the period of the seasonality
    * @param harmonics the number of harmonics to use in the seasonal model
    * @param sde a solution to a diffusion process representing the latent state
    */
private final case class SeasonalModel(
  period: Int, 
  harmonics: Int, 
  sde: Sde, p: LeafParameter) extends Model {

  def observation = x => p.scale match {
    case Some(v) => Gaussian(x.head, v)
    case None => throw new Exception("No SD parameter provided to SeasonalModel")
  }

  def buildF(harmonics: Int, t: Time): DenseVector[Double] = {
    val frequency = 2 * math.Pi / period
    val res = (1 to harmonics).toArray.
      flatMap (a => Array(cos(frequency * a * t), sin(frequency * a * t)))

    DenseVector(res)
  }

  def f(s: State, t: Time) = s.fold(0.0)(x => buildF(harmonics, t) dot x)(_ + _)

  def dataLikelihood = (eta, y) => p.scale match {
    case Some(v) => Gaussian(eta.head, v).logPdf(y)
    case None => throw new Exception("No SD parameter provided to SeasonalModel")
  }
}

  /**
    * A linear unparameterised model
    * @param sde a solution to a diffusion process representing the evolution of the latent state
    * @return an UnparamModel which can be composed with other models
    */
private final case class LinearModel(sde: Sde, p: LeafParameter) extends Model {
  def observation = x => p.scale match {
    case Some(v) => Gaussian(x.head, v)
    case None => throw new Exception("Must provide SD parameter for LinearModel")
  }
  
  def f(s: State, t: Time) = s.fold(0.0)((x: DenseVector[Double]) => x(0))(_ + _)

  def dataLikelihood = (eta, y) => p.scale match {
    case Some(v) =>  Gaussian(eta.head, v).logPdf(y)
    case None => throw new Exception("Must provide SD parameter for LinearModel")
  }
}

  /**
    * The Poisson unparameterised model with a one dimensional latent state
    * @param sde a solution to a diffusion process representing the evolution of the latent space
    * @return a Poisson UnparamModel which can be composed with other UnparamModels
    */
private final case class PoissonModel(sde: Sde, p: LeafParameter) extends Model {
  def observation = lambda => Poisson(lambda.head) map (_.toDouble): Rand[Double]

  override def link(x: Double) = Vector(exp(x))

  def f(s: State, t: Time) = s.fold(0.0)((x: DenseVector[Double]) => x(0))(_ + _)

  def dataLikelihood = (lambda, y) => Poisson(lambda.head).logProbabilityOf(y.toInt)
}

  /**
    * The bernoulli model with a one dimensional latent state
    * @param sde a solution to a diffusion process 
    */
private final case class BernoulliModel(sde: Sde, p: LeafParameter) extends Model {
  def observation = p => Uniform(0, 1).map(_ < p.head).map(a => if (a) 1.0 else 0.0)

  override def link(x: Gamma) = {
    if (x > 6) {
      Vector(1.0)
    } else if (x < -6) {
      Vector(0.0)
    } else {
      Vector(1.0/(1 + exp(-x)))
    }
  }


  def f(s: State, t: Time) = s.fold(0.0)((x: DenseVector[Double]) => x(0))(_ + _)

  def dataLikelihood = (p, y) => {
    if (y == 1.0) {
      if (p.head == 0.0) -1e99 else log(p.head)
    } else {
      if (p.head == 1.0) -1e99 else log(1-p.head)
    }
  }
}

  /**
    * The Log-Gaussian Cox-Process is used to model time to event data with 
    * log-gaussian varying hazard rate
    */
private final case class LogGaussianCox(sde: Sde, p: LeafParameter) extends Model {
  def observation = ???

  def f(s: State, t: Time) = s.fold(0.0)((x: DenseVector[Double]) => x(0))(_ + _)

  def dataLikelihood = (lambda, y) => lambda.head - lambda(1)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy