exchangealgebra-0.6.0.0: src/ExchangeAlgebra/Simulation/Analysis.hs
{-# LANGUAGE Strict #-}
{-# LANGUAGE StrictData #-}
{- |
Module : ExchangeAlgebra.Simulation.Analysis
Copyright : (c) Kaya Akagi. 2018-2026
Maintainer : yakagika@icloud.com
Released under the OWL license
Matrix analysis for ripple effects. Import this module to compute a
Leontief inverse or extract the response to one industry's demand.
== Operations
+----------------------------+--------------------------------------------+
| operation | result |
+============================+============================================+
| 'leontiefInverse' | Invert @I - A@ for input coefficients @A@. |
+----------------------------+--------------------------------------------+
| 'rippleEffect' | Fill one response column of a zero matrix. |
+----------------------------+--------------------------------------------+
Arrays use one-based bounds starting at @(1,1)@. @inverse@ does not
exchange pivot rows, so a mathematically invertible matrix such as
@[[0,1],[1,0]]@ can produce non-finite results. 'rippleEffect' does
not check the industry index before indexing the array.
@inverse@ modifies its input array. 'leontiefInverse' makes a copy
before inversion, leaving its input unchanged.
The accounting state space and the analysis utilities are discussed in:
Kaya Akagi. /Accounting State Space as the Minimal Unit for Economic/
/Agent-Based Modeling: Advancing Ripple Effect Analysis in Real-Time/
/Economy./ Research Square, preprint (Version 1), posted 5 January 2026.
<https://doi.org/10.21203/rs.3.rs-8485050/v1>
>>> import Data.Array.IO (newListArray, getElems, IOArray)
>>> a <- newListArray ((1,1),(2,2)) [0,0,0,0] :: IO (IOArray (Int,Int) Double)
>>> leontiefInverse a >>= getElems
[1.0,0.0,0.0,1.0]
Runnable ripple examples are preserved in the
<https://github.com/yakagika/ExchangeAlgebra/tree/archive/research-examples-pre-p4/examples archive/research-examples-pre-p4 tag>.
-}
module ExchangeAlgebra.Simulation.Analysis
( leontiefInverse
, rippleEffect
) where
import Control.Monad (forM_, when)
import Data.Array.IO (IOArray, getBounds, newArray, readArray, writeArray)
import Data.Ix (range)
import ExchangeAlgebra.Simulation.Array (modifyArray)
------------------------------------------------------------------
-- * Ripple Effect Analysis
------------------------------------------------------------------
-- | Allocate an identity matrix with bounds @((1,1),(n,n))@.
--
-- The caller supplies the dimension. This is an internal helper for
-- @inverse@; it does not validate a nonpositive dimension.
identity :: Int -> IO (IOArray (Int, Int) Double)
identity n = newArray ((1, 1), (n, n)) 0 >>= \arr -> do
forM_ [1..n] $ \i -> writeArray arr (i, i) 1
return arr
-- | Invert a one-based square matrix with Gauss-Jordan elimination.
--
-- This function changes the input array in place. It assumes bounds
-- @((1,1),(n,n))@; another origin causes a pattern-match failure.
-- It does not exchange pivot rows. A zero pivot, including one in an
-- invertible matrix such as @[[0,1],[1,0]]@, can yield non-finite values.
inverse :: IOArray (Int, Int) Double -> IO (IOArray (Int, Int) Double)
inverse mat = do
bnds <- getBounds mat
-- @inverse@ is only ever called on a 1-indexed square matrix, so the bounds
-- are @((1,1),(n,n))@; matching @((1,1),(n,_))@ is intentionally partial
-- (audited invariant) — a non-1-indexed matrix is a programmer error here.
let ((1,1),(n,_)) = bnds
inv <- identity n
forM_ [1..n] $ \i -> do
pivot <- readArray mat (i,i)
forM_ [1..n] $ \j -> do
modifyArray mat (i,j) (/pivot)
modifyArray inv (i,j) (/pivot)
forM_ [1..n] $ \k -> when (k /= i) $ do
factor <- readArray mat (k,i)
forM_ [1..n] $ \j -> do
mVal <- readArray mat (i,j)
iVal <- readArray inv (i,j)
modifyArray mat (k,j) (\x -> x - factor * mVal)
modifyArray inv (k,j) (\x -> x - factor * iVal)
return inv
{- | Compute the Leontief inverse @(I - A)^(-1)@ of an input-coefficient
matrix @A@.
The array must be square with origin @(1,1)@. This function copies @A@
before calling @inverse@, so the input array remains unchanged. The
current implementation does not swap pivot rows; a zero pivot can
produce non-finite values even when @I - A@ is invertible. A different
origin causes a pattern-match failure in @inverse@.
-}
leontiefInverse :: IOArray (Int, Int) Double -> IO (IOArray (Int, Int) Double)
leontiefInverse a = do
bnds <- getBounds a
temp <- newArray bnds 0
forM_ (range bnds) $ \(i,j) -> do
val <- readArray a (i,j)
writeArray temp (i,j) (if i == j then 1 - val else -val)
inverse temp
-- | Copy the response to a one-unit demand increase from one column of
-- a Leontief inverse.
--
-- The second argument is the Leontief inverse, not the input-coefficient
-- matrix. The function allocates a matrix with the same bounds, initializes
-- it to zero, and fills only the selected industry's column. It does not
-- validate the industry index; an out-of-range index causes an array
-- indexing error. Allocation is O(n²) for an n-by-n matrix, with O(n)
-- column reads and writes.
rippleEffect :: Int -> IOArray (Int, Int) Double -> IO (IOArray (Int, Int) Double)
rippleEffect industry inverseArr = do
((r1,c1),(r2,c2)) <- getBounds inverseArr
result <- newArray ((r1,c1),(r2,c2)) 0
forM_ [r1..r2] $ \i -> do
val <- readArray inverseArr (i, industry)
writeArray result (i, industry) val
return result