packages feed

dtmc-0.3.0.0: src/Dtmc/Analysis/Classification/Internal.hs

{-# LANGUAGE ExplicitNamespaces #-}

{- |
Module      : Dtmc.Analysis.Classification.Internal
Description : Internal carriers and construction for chain classification.

Raw carrier types behind "Dtmc.Analysis.Classification": the per-class summary
@CommClass@ and whole-chain structural report @Classification@. The report
builder is shared with the transition-matrix cache.
-}
module Dtmc.Analysis.Classification.Internal (
    type CommClass (..),
    type Classification (..),
    classificationFromGraph,
) where

import Dtmc.Transition.Matrix.Internal.Graph qualified as G
import Numeric.Natural (
    Natural,
 )

{- | Structural facts about one communicating class. For a finite valid DTMC,
a closed class consists of recurrent states.
-}
data CommClass state = CommClass
    { classMembers :: [state]
    -- ^ Member states in ascending order.
    , classPeriod :: Maybe Natural
    -- ^ Shared state period, or 'Nothing' when the class has no cycle.
    , classClosed :: Bool
    -- ^ Whether no positive-probability transition leaves the class.
    }

deriving instance (Eq state) => Eq (CommClass state)

deriving instance (Show state) => Show (CommClass state)

{- | A consistent structural report built by
'Dtmc.Analysis.Classification.classify'. The constructor is exposed here for
trusted internal use; "Dtmc.Analysis.Classification" keeps it hidden so its
summary fields stay aligned with its communicating classes.
-}
data Classification state = Classification
    { classesOf :: [CommClass state]
    -- ^ The communicating classes, ordered by least member.
    , isIrreducible :: Bool
    -- ^ Whether the states form a single (non-empty) communicating class.
    , isAperiodic :: Bool
    -- ^ Whether every class has period @1@ (and there is at least one class).
    , isErgodic :: Bool
    {- ^ Whether the chain is irreducible and aperiodic. For a finite DTMC this
    implies convergence to its unique stationary distribution.
    -}
    , chainPeriodOf :: Maybe Natural
    {- ^ The period of an irreducible chain (@Just d@), or @Nothing@ for a
    reducible chain, where period is a per-class notion, or when the single
    class has no cycles.
    -}
    , recurrentStatesOf :: [state]
    -- ^ States in closed classes, which are recurrent in a finite chain.
    , transientStatesOf :: [state]
    -- ^ States in non-closed classes, which are transient.
    , absorbingStatesOf :: [state]
    {- ^ Singleton closed classes. For exact stochastic rows these are
    absorbing states with @P(i,i) = 1@; numerically derived or otherwise
    unchecked rows are classified only by strict-positive support.
    -}
    }

type role Classification nominal

deriving instance (Eq state) => Eq (Classification state)

deriving instance (Show state) => Show (Classification state)

{- | Build a complete typed classification from an index-to-state conversion
and a shared support graph. Supplying the canonical conversion of a
'Dtmc.State.FiniteState' instance produces the public classification.

The result is deliberately lazy so a transition matrix can carry it without
forcing graph construction. Once forced, all whole-chain projections share
the same classes and state lists.

Complexity: @O(n + E)@ time after the graph's component and period caches are
available, @O(n)@ temporary space, and @O(n)@ retained result space.
-}
classificationFromGraph :: (Int -> state) -> G.Graph -> Classification state
classificationFromGraph toState graph =
    Classification
        { classesOf = communicating
        , isIrreducible = irreducible'
        , isAperiodic = aperiodic'
        , isErgodic = irreducible' && aperiodic'
        , chainPeriodOf = case communicating of
            [communicatingClass] -> classPeriod communicatingClass
            _ -> Nothing
        , recurrentStatesOf =
            concatMap classMembers (filter classClosed communicating)
        , transientStatesOf =
            concatMap classMembers (filter (not . classClosed) communicating)
        , absorbingStatesOf =
            [ state
            | communicatingClass <- communicating
            , classClosed communicatingClass
            , [state] <- [classMembers communicatingClass]
            ]
        }
  where
    components = G.components graph
    communicating =
        [ CommClass
            { classMembers = map toState component
            , classPeriod = G.periodOf graph first
            , classClosed = G.inClosedComponent graph first
            }
        | component@(first : _) <- components
        ]
    irreducible' = case components of
        [component] -> not (null component)
        _ -> False
    aperiodic' =
        not (null components)
            && all ((== Just 1) . G.componentPeriod graph) components
{-# INLINE classificationFromGraph #-}