packages feed

probability-polynomial-1.0.1.0: src/Numeric/Measure/Finite/Mixed.hs

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

{-|
Copyright   : Predictable Network Solutions Ltd., 2020-2024
License     : BSD-3-Clause
Description : Finite signed measures on the number line.
-}
module Numeric.Measure.Finite.Mixed
    ( -- * Type
      Measure
    , zero
    , dirac
    , uniform
    , distribution
    , fromDistribution

      -- * Observations
    , total
    , support
    , isPositive
    , integrate

      -- * Operations, numerical
    , add
    , scale
    , translate
    , beforeOrAt
    , after
    , convolve
    ) where

import Data.Function.Class
    ( Function (..)
    )
import Data.List
    ( scanl'
    )
import Control.DeepSeq
    ( NFData
    )
import Numeric.Function.Piecewise
    ( Piecewise
    )
import Numeric.Polynomial.Simple
    ( Poly
    , constant
    )

import qualified Data.Map.Strict as Map
import qualified Numeric.Function.Piecewise as Piecewise
import qualified Numeric.Measure.Discrete as D
import qualified Numeric.Polynomial.Simple as Poly

{-----------------------------------------------------------------------------
    Type
------------------------------------------------------------------------------}
-- | A finite
-- [signed measure](https://en.wikipedia.org/wiki/Signed_measure)
-- on the number line.
newtype Measure a = Measure (Piecewise (Poly a))
    -- INVARIANT: Adjacent pieces contain distinct objects.
    -- INVARIANT: The last piece is a constant polynomial,
    --            so that the measure is finite.
    deriving (Show, NFData)

-- | @eval (distribution m) x@ is the measure of the interval \( (-∞, x] \).
--
-- This is known as the [distribution function
-- ](https://en.wikipedia.org/wiki/Distribution_function_%28measure_theory%29).
distribution :: (Ord a, Num a) => Measure a -> Piecewise (Poly a)
distribution (Measure p) = p

-- | Construct a signed measure from its
-- [distribution function
-- ](https://en.wikipedia.org/wiki/Distribution_function_%28measure_theory%29).
--
-- Return 'Nothing' if the measure is not finite,
-- that is if the last piece of the piecewise function is not constant.
fromDistribution
    :: (Ord a, Num a)
    => Piecewise (Poly a) -> Maybe (Measure a)
fromDistribution pieces
    | isEventuallyConstant pieces = Just $ Measure $ trim pieces
    | otherwise = Nothing

-- | Test whether a piecewise polynomial is constant as x -> ∞.
isEventuallyConstant :: (Ord a, Num a) => Piecewise (Poly a) -> Bool
isEventuallyConstant pieces
    | null xpolys = True
    | otherwise = (<= 0) . Poly.degree . snd $ last xpolys
  where
    xpolys = Piecewise.toAscPieces pieces

-- | Internal.
-- Join all intervals whose polynomials are equal.
trim :: (Ord a, Num a) => Piecewise (Poly a) -> Piecewise (Poly a)
trim = Piecewise.trim

-- | Two measures are equal if they yield the same measures on every set.
--
-- > mx == my
-- >   implies
-- >   forall t. eval (distribution mx) t = eval (distribution my) t
instance (Ord a, Num a) => Eq (Measure a) where
    Measure mx == Measure my =
        Piecewise.toAscPieces mx == Piecewise.toAscPieces my

{-----------------------------------------------------------------------------
    Operations
------------------------------------------------------------------------------}
-- | The measure that assigns @0@ to every set.
zero :: Num a => Measure a
zero = Measure Piecewise.zero

-- | A
-- [Dirac measure](https://en.wikipedia.org/wiki/Dirac_measure)
-- at the given point @x@.
--
-- > total (dirac x) = 1
dirac :: (Ord a, Num a) => a -> Measure a
dirac x = Measure $ Piecewise.fromAscPieces [(x, Poly.constant 1)]

-- | The probability measure of a uniform probability distribution
-- in the interval \( [x,y) \).
--
-- > total (uniform x y) = 1
uniform :: (Ord a, Num a, Fractional a) => a -> a -> Measure a
uniform x y = Measure $ case compare x y of
    EQ -> Piecewise.fromAscPieces [(x, 1)]
    _  -> Piecewise.fromAscPieces [(low, poly), (high, 1)]
  where
    low = min x y
    high = max x y
    poly = Poly.lineFromTo (low, 0) (high, 1)

-- | The total of the measure applied to the set of real numbers.
total :: (Ord a, Num a) => Measure a -> a
total (Measure p) =
    case Piecewise.toAscPieces p of
        [] -> 0
        ps -> eval (snd (last ps)) 0

-- | The 'support' is the smallest closed, contiguous interval \( [x,y] \)
-- outside of which the measure is zero.
--
-- Returns 'Nothing' if the interval is empty.
support :: (Ord a, Num a) => Measure a -> Maybe (a, a)
support (Measure pieces) =
    case Piecewise.toAscPieces pieces of
        [] -> Nothing
        ps -> Just (fst $ head ps, fst $ last ps)

-- | Check whether a signed measure is positive.
--
-- A signed measure is /positive/ if the measure of any set
-- is nonnegative. In other words a positive signed measure
-- is just a measure in the ordinary sense.
--
-- This test is nontrivial, as we have to check that the distribution
-- function is monotonically increasing.
isPositive :: (Ord a, Num a, Fractional a) => Measure a -> Bool
isPositive (Measure m) = go 0 $ Piecewise.toAscPieces m
  where
    go _ [] =
        True
    go before ((x, o) : []) =
        eval before x <= eval o x
    go before ((x1, o) : xos@((x2, _) : _)) =
        (eval before x1 <= eval o x1)
        && Poly.isMonotonicallyIncreasingOn o (x1,x2)
        && go o xos

{-----------------------------------------------------------------------------
    Operations
    Numerical
------------------------------------------------------------------------------}
-- | Add two measures.
--
-- > total (add mx my) = total mx + total my
add :: (Ord a, Num a) => Measure a -> Measure a -> Measure a
add (Measure mx) (Measure my) =
    Measure $ trim $ Piecewise.zipPointwise (+) mx my

-- | Scale a measure by a constant.
--
-- > total (scale a mx) = a * total mx
scale :: (Ord a, Num a) => a -> Measure a -> Measure a
scale 0 (Measure _) = zero
scale x (Measure m) = Measure $ Piecewise.mapPieces (Poly.scale x) m

-- | Translate a measure along the number line.
--
-- > eval (distribution (translate y m)) x
-- >    = eval (distribution m) (x - y)
translate :: (Ord a, Num a, Fractional a) => a -> Measure a -> Measure a
translate y (Measure m) =
    Measure $ Piecewise.translateWith Poly.translate y m

{-----------------------------------------------------------------------------
    Operations
    Intersection
------------------------------------------------------------------------------}
-- | Intersect a measure with the interval @(-∞, x]@.
--
-- The measure of the interval @(-∞, t]@ with @beforeOrAt x m@ is the same as
-- the measure of the intersection @(-∞, t] ∩ (-∞, x]@ with @m@. 
beforeOrAt :: (Ord a, Num a) => a -> Measure a -> Measure a
beforeOrAt x (Measure m) =
    case Piecewise.toAscPieces m of
        [] -> zero
        ((x1, _):_) ->
            let indicatorToX = Piecewise.fromInterval (x1,x) 1
                scaledIndicatorAfterX v =
                    Piecewise.fromAscPieces [(x, constant v)]
            in  Measure
                $ trim
                $ indicatorToX * m + scaledIndicatorAfterX (eval m x)

-- | Intersect a measure with the interval @(x, +∞)@.
--
-- The measure of the interval @(-∞, t]@ with @after x m@ is the same as
-- the measure of the intersection @(-∞, t] ∩ (x, +∞)@ with @m@. 
after :: (Ord a, Num a) => a -> Measure a -> Measure a
after x (Measure m) =
    let scaledIndicatorAfterX v = Piecewise.fromAscPieces [(x, constant v)]
    in  Measure
        $ trim
        $ scaledIndicatorAfterX 1 * m
            - scaledIndicatorAfterX (eval m x)

{-----------------------------------------------------------------------------
    Operations
    Decomposition into continuous and discrete measures,
    needed for convolution.
------------------------------------------------------------------------------}
-- | Measure that is absolutely continuous
-- with respect to the Lebesgue measure,
-- Represented via its distribution function.
newtype Continuous a = Continuous { unContinuous :: Piecewise (Poly a) }
    -- INVARIANT: The last piece is @Poly.constant p@ for some @p :: a@.

-- | Density function (Radon–Nikodym derivative) of an absolutely
-- continuous measure.
newtype Density a = Density (Piecewise (Poly a))
    -- INVARIANT: The last piece is @Poly.constant 0@.

-- | Density function of an absolutely continuous measure.
toDensity
    :: (Ord a, Num a, Fractional a)
    => Continuous a -> Density a
toDensity = Density . Piecewise.mapPieces Poly.differentiate . unContinuous

-- | Decompose a mixed measure into
-- a continuous measure and a discrete measure.
-- See also [Lebesgue's decomposition theorem
-- ](https://en.wikipedia.org/wiki/Lebesgue%27s_decomposition_theorem)
decompose
    :: (Ord a, Num a, Fractional a)
    => Measure a -> (Continuous a, D.Discrete a)
decompose (Measure m) =
    ( Continuous $ trim $ Piecewise.fromAscPieces withoutJumps
    , D.fromMap $ Map.fromList jumps
    )
  where
    pieces = Piecewise.toAscPieces m

    withoutJumps =
        zipWith (\(x,o) j -> (x, o - Poly.constant j)) pieces totalJumps
    totalJumps = tail $ scanl' (+) 0 $ map snd jumps

    jumps = go 0 pieces
      where
        go _ [] = []
        go prev ((x,o) : xos) =
            (x, Poly.eval o x - Poly.eval prev x) : go o xos

{-----------------------------------------------------------------------------
    Observations
    Integration
------------------------------------------------------------------------------}
-- | Integrate a polynomial @f@ with respect to the given measure @m@,
-- \( \int f(x) dm(x) \).
integrate :: (Ord a, Num a, Fractional a) => Poly a -> Measure a -> a
integrate f m =
    integrateContinuous f continuous
    + D.integrate (eval f) discrete
  where
    (continuous, discrete) = decompose m

-- | Integrate a polynomial over an absolutely continuous measure.
integrateContinuous
    :: (Ord a, Num a, Fractional a)
    => Poly a -> Continuous a -> a
integrateContinuous f gg
    | null gpieces = 0
    | otherwise = sum $ map integrateOverInterval $ integrands
  where
    Density g = toDensity gg
    gpieces = Piecewise.toAscPieces g

    -- Pieces on the bounded intervals
    boundedPieces xos =
        zipWith (\(x1,o) (x2,_) -> ((x1, x2), o)) xos (drop 1 xos)

    integrands = [ (x12, f * o) | (x12, o) <- boundedPieces gpieces ]

    integrateOverInterval ((x1, x2), p) =
        eval pp x2 - eval pp x1
      where
        pp = Poly.integrate p

{-----------------------------------------------------------------------------
    Operations
    Convolution
------------------------------------------------------------------------------}
{-$ NOTE [Convolution]

In order to compute a convolution,
we convolve a density with the distribution function.

Let $f$ denote a density, which can be continuous or a Dirac delta.
Let $G$ denote a distribution function.
Let $H = f * G$ be the result of the convolution.
It can be shown that this is the distribution function of the
convolution of the densities, $h = f * g$.

The formula for convolution is

$ H(y) = ∫ f(y - x) G(x) dx = ∫ f (x) G(y - x) dx$.

When $f$ is a sum of delta functions, $f = Σ w_j delta_{x_j}(x)$,
this integral becomes ($y - x = x_j$ => $x = y - x_j$)

$ H(y) = Σ w_j G(y - x_j) $.

When $f$ is a piecewise polynomial, we can convolve the pieces.

When convolving with a distribution function, the final piece
will be a constant $g_n$ on the interval $[x_n,∞)$.
In this case, the convolution is given by

\[
H(y)
    = ∫ f (x) G(y - x) dx
    = ∫_{ -∞}^{y-x_n} f(x) g_n dx
    = g_n F(y-x_n)
\]

where $F$ is the distribution function of the density $f$.
-}

-- | Convolve a discrete measure with a mixed measure.
--
-- See NOTE [Convolution].
convolveDiscrete
    :: (Ord a, Num a, Fractional a)
    => D.Discrete a -> Measure a -> Measure a
convolveDiscrete f gg =
    foldr add zero
        [ scale w (translate x gg)
        | (x, w) <- Map.toAscList $ D.toMap f
        ]

-- | Convolve an absolutely continuous measure with a mixed measure.
--
-- See NOTE [Convolution].
convolveContinuous
    :: (Ord a, Num a, Fractional a)
    => Continuous a -> Measure a -> Measure a
convolveContinuous (Continuous ff) (Measure gg)
    | null ffpieces = zero
    | null ggpieces = zero
    | otherwise = Measure $ trim $ boundedConvolutions + lastConvolution
  where
    ffpieces = Piecewise.toAscPieces ff
    ggpieces = Piecewise.toAscPieces gg

    Density f = toDensity (Continuous ff)
    fpieces = Piecewise.toAscPieces f

    -- Pieces on the bounded intervals
    boundedPieces xos =
        zipWith (\(x,o) (y,_) -> (x, y, o)) xos (drop 1 xos)

    boundedConvolutions =
        sum $
            [ Piecewise.fromAscPieces (Poly.convolve fo ggo)
            | fo <- boundedPieces fpieces
            , ggo <- boundedPieces ggpieces
            ]

    (xlast, plast) = last ggpieces
    glast = case Poly.toCoefficients plast of
        [] -> 0
        (a0:_) -> a0
    lastConvolution =
        Piecewise.mapPieces (Poly.scale glast)
        $ Piecewise.translateWith Poly.translate xlast ff

-- | Additive convolution of two measures.
--
-- Properties:
--
-- > convolve (dirac x) (dirac y) = dirac (x + y)
-- >
-- > convolve mx my               =  convolve my mx
-- > convolve (add mx my) mz      =  add (convolve mx mz) (convolve my mz)
-- > translate z (convolve mx my) =  convolve (translate z mx) my
-- > total (convolve mx my)       =  total mx * total my
convolve
    :: (Ord a, Num a, Fractional a)
    => Measure a -> Measure a -> Measure a
convolve mx my =
    add (convolveContinuous contx my) (convolveDiscrete deltax my)
  where
    (contx, deltax) = decompose mx