packages feed

dtmc-0.2.0.0: src/Dtmc/Simplex/Internal.hs

{- |
Module      : Dtmc.Simplex.Internal
Description : Construction and repair of probability-simplex values.

Shared simplex construction for distribution and transition-matrix smart
constructors. Accepted values are made canonical by clamping tolerated bound
error and normalising the repaired total.
-}
module Dtmc.Simplex.Internal (
    simplexTolerance,
    canonicaliseSimplex,
    canonicaliseSimplexEntries,
) where

import Data.List qualified as List
import Dtmc.Simplex (
    SimplexError (..),
 )
import Numeric.LinearAlgebra qualified as LA

-- | The absolute tolerance shared by simplex construction and sampling repair.
simplexTolerance :: Double
simplexTolerance = 1e-9

{- | Construct a canonical simplex vector when every coordinate is in
@[-simplexTolerance, 1 + simplexTolerance]@ and its total is in
@[1 - simplexTolerance, 1 + simplexTolerance]@. Tolerated negative coordinates
are clamped to zero, tolerated coordinates above one are clamped to one, and
the repaired coordinates are divided by their computed total.

Reports the first non-finite or bound error before checking the total. An
empty vector yields @Left (SumOffBy 0)@.

Complexity: @O(n)@ time and @O(n)@ temporary and result space.
-}
canonicaliseSimplex :: LA.Vector Double -> Either SimplexError (LA.Vector Double)
canonicaliseSimplex vector =
    LA.fromList <$> canonicaliseSimplexEntries (LA.toList vector)

{- | Construct a canonical finite list with the same tolerance, repair, and
error ordering as 'canonicaliseSimplex'. Entry indices refer to the supplied
list order.

An empty list yields @Left (SumOffBy 0)@.

Complexity: @O(n)@ time and @O(n)@ temporary and result space.
-}
canonicaliseSimplexEntries :: [Double] -> Either SimplexError [Double]
canonicaliseSimplexEntries entries =
    case firstInvalidEntry 0 entries of
        Just err -> Left err
        Nothing
            | abs (total - 1.0) <= simplexTolerance ->
                Right (map (/ repairedTotal) repaired)
            | otherwise -> Left (SumOffBy total)
  where
    total = List.foldl' (+) 0 entries
    repaired = map repair entries
    repairedTotal = List.foldl' (+) 0 repaired

    repair entry
        | entry < 0 = 0
        | entry > 1 = 1
        | otherwise = entry

-- Scan separately so a coordinate error reports its index before the total.
firstInvalidEntry :: Int -> [Double] -> Maybe SimplexError
firstInvalidEntry _ [] = Nothing
firstInvalidEntry index (entry : rest)
    | isNaN entry || isInfinite entry =
        Just (NonFiniteEntry index)
    | entry < negate simplexTolerance =
        Just (NegativeEntry index entry)
    | entry > 1.0 + simplexTolerance =
        Just (EntryAboveOne index entry)
    | otherwise =
        firstInvalidEntry (index + 1) rest