dtmc-0.2.0.0: src/Dtmc/Transition/Matrix/Internal.hs
{- |
Module : Dtmc.Transition.Matrix.Internal
Description : Raw carrier for transition matrices (unsafe underbelly).
Raw carrier behind t'Dtmc.Transition.Matrix.TransitionMatrix': an hmatrix
matrix paired with its lazy support graph. The public smart constructor
validates its square shape and canonicalises rows; this internal module
exposes unchecked construction.
The constructor is positional so the public matrix projection cannot act as a
record-update setter and desynchronise the matrix from its cached graph.
-}
module Dtmc.Transition.Matrix.Internal (
TransitionMatrix (TransitionMatrix),
unTransitionMatrix,
tmSupport,
unsafeTransitionMatrix,
matrixRowAt,
) where
import Dtmc.Distribution.Map (
fromDistribution,
)
import Dtmc.Distribution.Vector.Internal (
DistributionVector (DistributionVector),
)
import Dtmc.State (
FiniteState,
)
import Dtmc.State.Internal (
stateCardinalityInt,
stateIndexInt,
)
import Dtmc.Transition (
Transition (..),
)
import Dtmc.Transition.Matrix.Internal.Graph (
Graph,
fromAdjacency,
)
import Numeric.LinearAlgebra qualified as LA
{- | A stored square matrix whose rows and columns follow the canonical order
of its finite state type. Entry @(i,j)@ is the transition probability from
state @i@ to state @j@. 'Dtmc.Transition.Matrix.fromKernel' materialises
already-validated rows, while 'Dtmc.Transition.Matrix.fromRows' applies
tolerant row validation and canonicalisation. The internal constructor and
arithmetic instances do not revalidate.
Each value also carries its support graph as a /lazy/ second argument, so any
graph-based analyses on the same value share one build. Construct internal
values with @unsafeTransitionMatrix@ rather than pairing a matrix and graph
directly.
-}
data TransitionMatrix state
= -- | Unchecked matrix/cache pair; the graph must match the matrix.
TransitionMatrix (LA.Matrix Double) Graph
-- Nominal role prevents coercion between distinct state types, including
-- state types with the same cardinality.
type role TransitionMatrix nominal
{- | Return the stored matrix unchanged without forcing the support graph.
Complexity: @O(1)@ time and @O(1)@ space.
-}
unTransitionMatrix ::
TransitionMatrix state ->
LA.Matrix Double
unTransitionMatrix (TransitionMatrix matrix _) = matrix
{- | Return the lazy support graph, with edge @i -> j@ exactly when the stored
entry is strictly positive. No tolerance is applied: a tiny positive rounding
value creates an edge, while zero or a negative value does not.
The result is shared by later analyses of the same value.
Complexity: @O(1)@ projection time and @O(1)@ projection space. The first
analysis that forces the graph takes @O(n^2)@ time and @O(n^2)@ temporary
space; the resulting graph occupies @O(n + E)@ space for @E@ support edges.
-}
tmSupport :: TransitionMatrix state -> Graph
tmSupport (TransitionMatrix _ support) = support
-- Manual 'Show': 'Graph' has no 'Show', and the derived cache should not
-- appear in the rendering.
instance Show (TransitionMatrix state) where
showsPrec d p =
showParen (d > 10) $
showString "TransitionMatrix "
. showsPrec 11 (unTransitionMatrix p)
{- | Pair a raw matrix with its lazy support graph. This performs no
row-stochastic, finiteness, or simplex validation; internal callers must
establish the required invariant.
Complexity: @O(1)@ construction time and @O(1)@ construction space. Forcing
the support graph takes @O(n^2)@ time and @O(n^2)@ temporary space; the graph
occupies @O(n + E)@ space for @E@ support edges.
-}
unsafeTransitionMatrix ::
LA.Matrix Double ->
TransitionMatrix state
unsafeTransitionMatrix matrix =
TransitionMatrix matrix (supportGraphOf matrix)
{- | Wrap one stored matrix row as a distribution vector without revalidation.
The finite-state index makes the lookup total.
Complexity: excluding 'Dtmc.State.stateIndex', @O(n)@ time and @O(n)@ result
space for state cardinality @n@.
-}
matrixRowAt ::
(FiniteState state) =>
TransitionMatrix state ->
state ->
DistributionVector state
matrixRowAt matrix state = DistributionVector row
where
stored = unTransitionMatrix matrix
row =
LA.flatten
( LA.subMatrix
(stateIndexInt state, 0)
(1, LA.cols stored)
stored
)
instance (FiniteState state) => Transition (TransitionMatrix state) where
type TransitionState (TransitionMatrix state) = state
transitionLaw matrix =
fromDistribution . matrixRowAt matrix
-- Use strict positivity without tolerance so graph queries reflect the stored
-- matrix exactly; keep construction here so the cache cannot become stale.
supportGraphOf ::
LA.Matrix Double ->
Graph
supportGraphOf matrix =
fromAdjacency
dim
[ ((i, j), entry > 0)
| (i, row) <- zip [0 ..] rows
, (j, entry) <- zip [0 ..] row
]
where
rows = LA.toLists matrix
dim = length rows
{- | Matrix multiplication as transition composition: @p '<>' q@ takes a @p@
step followed by a @q@ step. Exact products preserve row-stochasticity and
associativity; 'Double' results are neither revalidated nor exactly
associative.
-}
instance Semigroup (TransitionMatrix state) where
(<>) ::
TransitionMatrix state ->
TransitionMatrix state ->
TransitionMatrix state
p <> q = unsafeTransitionMatrix (unTransitionMatrix p LA.<> unTransitionMatrix q)
{- | The identity matrix represents zero transitions and is the unit of the
transition-composition monoid.
-}
instance (FiniteState state) => Monoid (TransitionMatrix state) where
mempty :: TransitionMatrix state
mempty = unsafeTransitionMatrix (LA.ident (stateCardinalityInt @state))