dtmc-0.3.0.0: src/Dtmc/Analysis/LinearSystem/Internal.hs
{- |
Module : Dtmc.Analysis.LinearSystem.Internal
Description : @(I - Q)@ linear solves and sub-block extraction.
Dynamic linear algebra for DTMC hitting and return calculations: extract
blocks indexed by runtime state sets and solve systems of the form
@(I - Q) x = b@. Public modules convert bounded state indices to 'Int' and
keep dynamically sized matrices inside the implementation.
All arithmetic uses 'Double'. Every solve validates finiteness, rejects a
reciprocal condition estimate below @1e-12@, and verifies a scaled residual
against @1e-9@.
-}
module Dtmc.Analysis.LinearSystem.Internal (
subMatrix,
rowSums,
solveLinearSystem,
solveIminusQ,
solveIminusQVector,
fundamental,
) where
import Data.List qualified as List
import Data.Vector.Storable qualified as Storable
import Dtmc.Analysis.LinearSystem (
LinearSystemError (..),
)
import Numeric.LinearAlgebra qualified as LA
conditionLimit :: Double
conditionLimit = 1e-12
residualLimitValue :: Double
residualLimitValue = 1e-9
allFinite :: LA.Matrix Double -> Bool
allFinite = Storable.all isFinite . LA.flatten
isFinite :: Double -> Bool
isFinite value = not (isNaN value || isInfinite value)
infinityNorm :: LA.Matrix Double -> Double
infinityNorm = maximumAbsoluteSum . LA.toRows
oneNorm :: LA.Matrix Double -> Double
oneNorm = maximumAbsoluteSum . LA.toColumns
maximumAbsoluteSum :: [LA.Vector Double] -> Double
maximumAbsoluteSum =
List.foldl' max 0
. map (Storable.foldl' (\acc value -> acc + abs value) 0)
maximumAbsolute :: LA.Vector Double -> Double
maximumAbsolute = Storable.foldl' (\acc value -> max acc (abs value)) 0
reciprocalConditionForIminusQ ::
LA.Matrix Double ->
LA.Vector Double ->
Double
reciprocalConditionForIminusQ coefficient inverseTimesOne
| Storable.all isFinite inverseTimesOne
&& isFinite provenLowerBound
&& provenLowerBound >= conditionLimit =
provenLowerBound
| otherwise = LA.rcond coefficient
where
inverseInfinityNorm = maximumAbsolute inverseTimesOne
-- The inverse one-norm is at most n times its infinity norm, and every
-- spectral norm is at most the square root of one-norm times infinity norm.
provenLowerBound =
1
/ ( inverseInfinityNorm
* sqrt
( fromIntegral (LA.rows coefficient)
* oneNorm coefficient
* infinityNorm coefficient
)
)
relativeSystemResidual ::
LA.Matrix Double ->
LA.Matrix Double ->
LA.Matrix Double ->
Double
relativeSystemResidual coefficient solution rightHandSide =
infinityNorm (coefficient LA.<> solution - rightHandSide)
/ max
1
( infinityNorm coefficient * infinityNorm solution
+ infinityNorm rightHandSide
)
validateSolution ::
LA.Matrix Double ->
LA.Matrix Double ->
LA.Matrix Double ->
Double ->
Either LinearSystemError (LA.Matrix Double)
validateSolution coefficient rightHandSide solution reciprocalCondition
| not (allFinite solution) = Left NonFiniteSolution
| not (isFinite reciprocalCondition) = Left NonFiniteSystem
| reciprocalCondition < conditionLimit =
Left (IllConditionedSystem reciprocalCondition)
| residual > residualLimitValue =
Left
( ResidualTooLarge
{ relativeResidual = residual
, residualLimit = residualLimitValue
}
)
| otherwise = Right solution
where
residual = relativeSystemResidual coefficient solution rightHandSide
{- | Solve @A X = B@ by LU decomposition. @A@ must be a non-empty square
matrix, @B@ must have the same number of rows, and all entries must be finite;
incompatible dimensions raise a backend error.
Returns a 'LinearSystemError' when the input or result is non-finite, the
backend reports singularity, the reciprocal condition estimate is below
@1e-12@, or the scaled infinity-norm residual exceeds @1e-9@.
Complexity: @O(n^3 + n^2 r)@ time, @O(n^2 + n r)@ temporary space, and
@O(n r)@ result space for an @n x n@ coefficient matrix and @r@ right-hand
sides.
-}
solveLinearSystem ::
LA.Matrix Double ->
LA.Matrix Double ->
Either LinearSystemError (LA.Matrix Double)
solveLinearSystem coefficient rightHandSide
| not (allFinite coefficient && allFinite rightHandSide) =
Left NonFiniteSystem
| otherwise =
case LA.linearSolve coefficient rightHandSide of
Nothing -> Left SingularSystem
Just solution ->
validateSolution
coefficient
rightHandSide
solution
reciprocalCondition
where
reciprocalCondition = LA.rcond coefficient
{- | Extract the block of @m@ selected by the row and column indices. Their
order and multiplicity are preserved; an empty list produces a zero-sized
dimension.
Row indices must be in @{0 .. rows(m)-1}@ and column indices in
@{0 .. cols(m)-1}@; otherwise the backend raises an error.
Complexity: @O(R + C + R C)@ time, @O(R + C)@ temporary space, and
@O(R C)@ result space for @R@ selected rows and @C@ selected columns.
-}
subMatrix :: [Int] -> [Int] -> LA.Matrix Double -> LA.Matrix Double
subMatrix rowIdx colIdx m =
m LA.?? (LA.Pos (LA.idxs rowIdx), LA.Pos (LA.idxs colIdx))
{- | Compute the vector of row sums of @m@, equivalently @m@ applied to a
vector of ones. For a block @P[D, A]@ of a transition matrix, this is the
one-step probability of transitioning from each state in @D@ directly into
@A@. Results use ordinary floating-point summation and are not clamped to
@[0, 1]@.
Complexity: @O(R C + R + C)@ time, @O(C)@ temporary space, and @O(R)@
result space for an @R x C@ matrix.
-}
rowSums :: LA.Matrix Double -> LA.Vector Double
rowSums m = m LA.#> LA.konst 1 (LA.cols m)
{- | Solve @(I - Q) X = B@ by one LU decomposition. @Q@ must be a
transient-to-transient, substochastic @n x n@ matrix, @B@ must be @n x r@,
and all entries must be finite; incompatible dimensions raise a backend
error.
Returns a 'LinearSystemError' when the input or result is non-finite, the
backend reports singularity, the reciprocal condition estimate is below
@1e-12@, or the scaled infinity-norm residual exceeds @1e-9@.
The solve appends a column of ones to @B@. Since @(I - Q)^-1@ is
non-negative, the extra result gives its exact infinity norm in exact
arithmetic. Matrix-norm inequalities then provide a conservative lower bound
on the reciprocal 2-norm condition number. A bound above the acceptance limit
proves that the solution is sufficiently conditioned without a separate
singular-value decomposition; uncertain cases retain the backend condition
estimate and therefore the same rejection threshold.
Complexity: @O(n^3 + n^2 r)@ time, @O(n^2 + n r)@ temporary space, and
@O(n r)@ result space for @r@ right-hand sides.
-}
solveIminusQ ::
LA.Matrix Double ->
LA.Matrix Double ->
Either LinearSystemError (LA.Matrix Double)
solveIminusQ q rightHandSide
| not (allFinite q && allFinite rightHandSide) = Left NonFiniteSystem
| otherwise =
case LA.linearSolve coefficient augmentedRightHandSide of
Nothing -> Left SingularSystem
Just augmentedSolution ->
let solution =
LA.takeColumns
rightHandSideColumns
augmentedSolution
inverseTimesOne =
LA.flatten
( LA.dropColumns
rightHandSideColumns
augmentedSolution
)
in validateSolution
coefficient
rightHandSide
solution
( reciprocalConditionForIminusQ
coefficient
inverseTimesOne
)
where
coefficient = LA.ident (LA.rows q) - q
rightHandSideColumns = LA.cols rightHandSide
augmentedRightHandSide =
rightHandSide LA.||| LA.asColumn (LA.konst 1 (LA.rows q))
{- | Solve @(I - Q) x = b@ for a vector of length @n@. This has the same
shape requirements, numerical behaviour, and errors as 'solveIminusQ'.
Complexity: @O(n^3)@ time, @O(n^2)@ temporary space, and @O(n)@ result
space.
-}
solveIminusQVector ::
LA.Matrix Double ->
LA.Vector Double ->
Either LinearSystemError (LA.Vector Double)
solveIminusQVector q b =
LA.flatten <$> solveIminusQ q (LA.asColumn b)
{- | Compute @(I - Q)^-1@ by solving @(I - Q) G = I@. When @Q@ is a
transient-to-transient transition block with spectral radius below one, this
is the fundamental matrix @sum_{k=0}^infinity Q^k@.
This requires a non-empty square matrix with finite entries and inherits the
validation and error behaviour of 'solveIminusQ'.
Complexity: @O(n^3)@ time and @O(n^2)@ temporary and result space.
-}
fundamental :: LA.Matrix Double -> Either LinearSystemError (LA.Matrix Double)
fundamental q =
solveIminusQ q (LA.ident (LA.rows q))