packages feed

moonlight-homology-0.1.0.0: src-matrix/Moonlight/Homology/Boundary/LinAlg.hs

module Moonlight.Homology.Boundary.LinAlg
  ( BoundaryScope (..),
    ScopedBoundary (..),
    BoundaryIncidenceShapeError (..),
    BoundaryEntry,
    sourceIndex,
    targetIndex,
    boundaryCoefficient,
    mkBoundaryEntry,
    mkBoundaryEntryFromInts,
    BoundaryIncidence,
    sourceCardinality,
    targetCardinality,
    boundaryEntries,
    mkBoundaryIncidence,
    mkBoundaryIncidenceFromOrderedEntries,
    overlapBoundaryIncidence,
    emptyBoundaryIncidence,
    emptyBoundaryIncidenceOf,
    identityBoundaryIncidenceOf,
    directSumBoundaryIncidence,
    reindexBoundaryIncidenceWith,
    boundaryIncidenceApply,
    transposeBoundaryIncidence,
    composeBoundaryIncidence,
    boundaryIncidenceDiagonal,
    addBoundaryIncidence,
    mapBoundaryCoefficients,
    BlockBoundaryEntry,
    blockSourceIndex,
    blockTargetIndex,
    blockSubmatrix,
    mkBlockBoundaryEntry,
    BlockBoundaryIncidence,
    blockSourceDimensions,
    blockTargetDimensions,
    blockEntries,
    mkBlockBoundaryIncidence,
    flattenBlockIncidence,
    scaleBoundaryIncidence,
    materializeIncidenceBoundary,
    materializeBoundary,
  )
where

import Data.Function ((&))
import Data.Kind (Type)
import qualified Data.Map.Strict as Map
import Data.Maybe (listToMaybe, mapMaybe)
import Moonlight.Core (AdditiveMonoid (..), Semiring)
import Moonlight.Homology.Pure.Failure (HomologyFailure (..), HomologyLaw (..))
import Numeric.Natural (Natural)
import Moonlight.Pale.Diagnostic.Topology.Boundary (BoundaryIncidenceShapeError (..))

type BoundaryScope :: Type
data BoundaryScope
  = IncidenceScope
  | PortalScope
  deriving stock (Eq, Ord, Show)

type ScopedBoundary :: Type -> Type
data ScopedBoundary boundary = ScopedBoundary
  { boundaryScope :: BoundaryScope,
    boundaryValue :: boundary
  }
  deriving stock (Eq, Ord, Show)

type BoundaryEntry :: Type -> Type
data BoundaryEntry r = BoundaryEntry
  { sourceIndex :: Int,
    targetIndex :: Int,
    boundaryCoefficient :: r
  }
  deriving stock (Eq, Show)

mkBoundaryEntry :: Natural -> Natural -> r -> BoundaryEntry r
mkBoundaryEntry sourceIndexValue targetIndexValue coefficientValue =
  BoundaryEntry
    { sourceIndex = fromIntegral sourceIndexValue,
      targetIndex = fromIntegral targetIndexValue,
      boundaryCoefficient = coefficientValue
    }

mkBoundaryEntryFromInts :: Int -> Int -> r -> BoundaryEntry r
mkBoundaryEntryFromInts sourceIndexValue targetIndexValue coefficientValue =
  BoundaryEntry
    { sourceIndex = sourceIndexValue,
      targetIndex = targetIndexValue,
      boundaryCoefficient = coefficientValue
    }

type BoundaryIncidence :: Type -> Type
data BoundaryIncidence r = BoundaryIncidence
  { sourceCardinality :: Int,
    targetCardinality :: Int,
    boundaryEntries :: [BoundaryEntry r]
  }
  deriving stock (Eq, Show)

mkBoundaryIncidence :: (Eq r, Semiring r) => Natural -> Natural -> [BoundaryEntry r] -> Either BoundaryIncidenceShapeError (BoundaryIncidence r)
mkBoundaryIncidence sourceCardinalityValue targetCardinalityValue entries =
  let sourceDimension = fromIntegral sourceCardinalityValue
      targetDimension = fromIntegral targetCardinalityValue
   in case firstOutOfBoundsEntry sourceDimension targetDimension entries of
        Just entryValue -> Left (entryOutOfBoundsError sourceDimension targetDimension entryValue)
        Nothing -> Right (uncheckedBoundaryIncidence sourceDimension targetDimension (canonicalizeEntries entries))

-- | Canonicalize already ordered sparse entries without constructing a map.
-- If the entries are not ordered by @(sourceIndex, targetIndex)@, this falls
-- back to the fully general constructor, preserving the exact semantics of
-- 'mkBoundaryIncidence' while giving inspected/preallocated callers a linear
-- hot path.
mkBoundaryIncidenceFromOrderedEntries :: (Eq r, Semiring r) => Natural -> Natural -> [BoundaryEntry r] -> Either BoundaryIncidenceShapeError (BoundaryIncidence r)
mkBoundaryIncidenceFromOrderedEntries sourceCardinalityValue targetCardinalityValue entries =
  let sourceDimension = fromIntegral sourceCardinalityValue
      targetDimension = fromIntegral targetCardinalityValue
   in case firstOutOfBoundsEntry sourceDimension targetDimension entries of
        Just entryValue -> Left (entryOutOfBoundsError sourceDimension targetDimension entryValue)
        Nothing ->
          case canonicalizeOrderedEntries entries of
            Nothing ->
              mkBoundaryIncidence sourceCardinalityValue targetCardinalityValue entries
            Just canonicalEntries ->
              Right (uncheckedBoundaryIncidence sourceDimension targetDimension canonicalEntries)

overlapBoundaryIncidence :: (Ord atom, Eq r, Semiring r) => r -> [atom] -> [atom] -> BoundaryIncidence r
overlapBoundaryIncidence coefficient sourceBasis targetBasis =
  uncheckedBoundaryIncidence (length sourceBasis) (length targetBasis)
    . canonicalizeEntries
    . Map.elems
    $ Map.intersectionWith
      (\sourceIdx targetIdx -> mkBoundaryEntry sourceIdx targetIdx coefficient)
      (basisIndex sourceBasis)
      (basisIndex targetBasis)

basisIndex :: Ord atom => [atom] -> Map.Map atom Natural
basisIndex =
  Map.fromList . flip zip [0 ..]

canonicalizeEntries :: (Eq r, Semiring r) => [BoundaryEntry r] -> [BoundaryEntry r]
canonicalizeEntries entries =
  entries
    & fmap (\entry -> ((sourceIndex entry, targetIndex entry), boundaryCoefficient entry))
    & Map.fromListWith add
    & Map.toAscList
    & mapMaybe
      ( \((sourceValue, targetValue), coefficientValue) ->
          if coefficientValue == zero
            then Nothing
            else
              Just
                ( mkBoundaryEntry
                    (fromIntegral sourceValue)
                    (fromIntegral targetValue)
                    coefficientValue
                )
      )

data OrderedCanonicalization r
  = OrderedEntriesOutOfOrder
  | OrderedCanonicalization !(Maybe (BoundaryEntry r)) ![BoundaryEntry r]

canonicalizeOrderedEntries :: (Eq r, Semiring r) => [BoundaryEntry r] -> Maybe [BoundaryEntry r]
canonicalizeOrderedEntries entries =
  finalizeOrderedCanonicalization
    ( foldl'
        appendOrderedEntry
        (OrderedCanonicalization Nothing [])
        entries
    )

appendOrderedEntry :: (Eq r, Semiring r) => OrderedCanonicalization r -> BoundaryEntry r -> OrderedCanonicalization r
appendOrderedEntry OrderedEntriesOutOfOrder _ =
  OrderedEntriesOutOfOrder
appendOrderedEntry (OrderedCanonicalization Nothing reversedCanonicalEntries) entry =
  OrderedCanonicalization (Just entry) reversedCanonicalEntries
appendOrderedEntry (OrderedCanonicalization (Just pendingEntry) reversedCanonicalEntries) entry =
  case compareBoundaryEntryCoordinate pendingEntry entry of
    GT ->
      OrderedEntriesOutOfOrder
    EQ ->
      -- Force the summed coefficient: a run of equal-coordinate entries on
      -- this advertised linear hot path must not chain 'add' thunks.
      let !summedCoefficient =
            add
              (boundaryCoefficient pendingEntry)
              (boundaryCoefficient entry)
       in OrderedCanonicalization
            (Just (pendingEntry {boundaryCoefficient = summedCoefficient}))
            reversedCanonicalEntries
    LT ->
      OrderedCanonicalization
        (Just entry)
        (prependNonZeroBoundaryEntry pendingEntry reversedCanonicalEntries)

finalizeOrderedCanonicalization :: (Eq r, Semiring r) => OrderedCanonicalization r -> Maybe [BoundaryEntry r]
finalizeOrderedCanonicalization OrderedEntriesOutOfOrder =
  Nothing
finalizeOrderedCanonicalization (OrderedCanonicalization pendingEntry reversedCanonicalEntries) =
  Just
    ( reverse
        ( maybe
            reversedCanonicalEntries
            (`prependNonZeroBoundaryEntry` reversedCanonicalEntries)
            pendingEntry
        )
    )

prependNonZeroBoundaryEntry :: (Eq r, Semiring r) => BoundaryEntry r -> [BoundaryEntry r] -> [BoundaryEntry r]
prependNonZeroBoundaryEntry entry entries =
  if boundaryCoefficient entry == zero
    then entries
    else entry : entries

compareBoundaryEntryCoordinate :: BoundaryEntry r -> BoundaryEntry r -> Ordering
compareBoundaryEntryCoordinate left right =
  compare
    (sourceIndex left, targetIndex left)
    (sourceIndex right, targetIndex right)


emptyBoundaryIncidence :: BoundaryIncidence r
emptyBoundaryIncidence =
  uncheckedBoundaryIncidence 0 0 []

emptyBoundaryIncidenceOf :: Natural -> Natural -> BoundaryIncidence r
emptyBoundaryIncidenceOf sourceCardinalityValue targetCardinalityValue =
  uncheckedBoundaryIncidence
    (fromIntegral sourceCardinalityValue)
    (fromIntegral targetCardinalityValue)
    []

identityBoundaryIncidenceOf :: Num r => Natural -> BoundaryIncidence r
identityBoundaryIncidenceOf dimensionValue =
  uncheckedBoundaryIncidence dimension dimension entries
  where
    dimension = fromIntegral dimensionValue
    entries =
      fmap
        (\index -> mkBoundaryEntry (fromIntegral index) (fromIntegral index) 1)
        (take dimension [0 :: Int ..])

directSumBoundaryIncidence :: BoundaryIncidence r -> BoundaryIncidence r -> BoundaryIncidence r
directSumBoundaryIncidence left right =
  uncheckedBoundaryIncidence
    (sourceCardinality left + sourceCardinality right)
    (targetCardinality left + targetCardinality right)
    ( boundaryEntries left
        <> fmap
          ( \entry ->
              mkBoundaryEntry
                (fromIntegral (sourceCardinality left + sourceIndex entry))
                (fromIntegral (targetCardinality left + targetIndex entry))
                (boundaryCoefficient entry)
          )
          (boundaryEntries right)
    )

reindexBoundaryIncidenceWith ::
  (Int -> Maybe Natural) ->
  (Int -> Maybe Natural) ->
  (BoundaryEntry a -> Maybe b) ->
  BoundaryIncidence a ->
  BoundaryIncidence b
reindexBoundaryIncidenceWith sourceReindex targetReindex coefficientAt incidence =
  uncheckedBoundaryIncidence
    (mappingDimension sourceReindex (sourceCardinality incidence))
    (mappingDimension targetReindex (targetCardinality incidence))
    ( mapMaybe
        ( \entry ->
            mkBoundaryEntry
              <$> sourceReindex (sourceIndex entry)
              <*> targetReindex (targetIndex entry)
              <*> coefficientAt entry
        )
        (boundaryEntries incidence)
    )

boundaryIncidenceApply :: Num r => BoundaryIncidence r -> Map.Map Int r -> Map.Map Int r
boundaryIncidenceApply incidence vectorValues =
  boundaryEntries incidence
    & fmap
      ( \entry ->
          ( targetIndex entry,
            boundaryCoefficient entry * Map.findWithDefault 0 (sourceIndex entry) vectorValues
          )
      )
    & Map.fromListWith (+)

transposeBoundaryIncidence :: BoundaryIncidence r -> BoundaryIncidence r
transposeBoundaryIncidence incidence =
  uncheckedBoundaryIncidence
    (targetCardinality incidence)
    (sourceCardinality incidence)
    ( boundaryEntries incidence
        & fmap
          ( \entry ->
              mkBoundaryEntry
                (fromIntegral (targetIndex entry))
                (fromIntegral (sourceIndex entry))
                (boundaryCoefficient entry)
          )
    )

composeBoundaryIncidence :: (Eq r, Num r, Semiring r) => BoundaryIncidence r -> BoundaryIncidence r -> Either BoundaryIncidenceShapeError (BoundaryIncidence r)
composeBoundaryIncidence left right =
  if targetCardinality right /= sourceCardinality left
    then
      Left
        ( BoundaryIncidenceShapeMismatch
            (sourceCardinality left)
            (targetCardinality left)
            (sourceCardinality right)
            (targetCardinality right)
        )
    else
      if null (boundaryEntries left) || null (boundaryEntries right)
        then
          Right
            ( emptyBoundaryIncidenceOf
                (fromIntegral (sourceCardinality right))
                (fromIntegral (targetCardinality left))
            )
        else
          let rightByTarget =
                boundaryEntries right
                  & fmap (\entry -> (targetIndex entry, [entry]))
                  & Map.fromListWith (<>)
              leftBySource =
                boundaryEntries left
                  & fmap (\entry -> (sourceIndex entry, [entry]))
                  & Map.fromListWith (<>)
              productTerms =
                Map.intersectionWith (,) rightByTarget leftBySource
                  & Map.elems
                  >>= ( \(rightBucket, leftBucket) ->
                          rightBucket
                            >>= ( \rightEntry ->
                                    leftBucket
                                      & fmap
                                        ( \leftEntry ->
                                            ( (sourceIndex rightEntry, targetIndex leftEntry),
                                              boundaryCoefficient leftEntry * boundaryCoefficient rightEntry
                                            )
                                        )
                                )
                      )
              composedEntries =
                productTerms
                  & Map.fromListWith (+)
                  & Map.toList
                  & fmap
                    ( \((sourceValue, targetValue), coefficientValue) ->
                        mkBoundaryEntry
                          (fromIntegral sourceValue)
                          (fromIntegral targetValue)
                          coefficientValue
                    )
           in mkBoundaryIncidence
                (fromIntegral (sourceCardinality right))
                (fromIntegral (targetCardinality left))
                composedEntries

boundaryIncidenceDiagonal :: Num r => BoundaryIncidence r -> Map.Map Int r
boundaryIncidenceDiagonal incidence =
  boundaryEntries incidence
    & filter (\entry -> sourceIndex entry == targetIndex entry)
    & fmap (\entry -> (sourceIndex entry, boundaryCoefficient entry))
    & Map.fromListWith (+)

addBoundaryIncidence ::
  (Eq r, Num r, Semiring r) =>
  BoundaryIncidence r ->
  BoundaryIncidence r ->
  Either BoundaryIncidenceShapeError (BoundaryIncidence r)
addBoundaryIncidence left right =
  if sourceCardinality left == sourceCardinality right
      && targetCardinality left == targetCardinality right
    then
      let mergedEntries =
            boundaryEntries left
              <> boundaryEntries right
              & fmap
                ( \entry ->
                    ( (sourceIndex entry, targetIndex entry),
                      boundaryCoefficient entry
                    )
                )
              & Map.fromListWith (+)
       in
        mkBoundaryIncidence
          (fromIntegral (sourceCardinality left))
          (fromIntegral (targetCardinality left))
          ( mergedEntries
              & Map.toList
              & fmap
                ( \((sourceValue, targetValue), coefficientValue) ->
                    mkBoundaryEntry
                      (fromIntegral sourceValue)
                      (fromIntegral targetValue)
                      coefficientValue
                )
          )
    else
      Left
        ( BoundaryIncidenceShapeMismatch
            (sourceCardinality left)
            (targetCardinality left)
            (sourceCardinality right)
            (targetCardinality right)
        )

mapBoundaryCoefficients :: (a -> b) -> BoundaryIncidence a -> BoundaryIncidence b
mapBoundaryCoefficients f incidence =
  uncheckedBoundaryIncidence
    (sourceCardinality incidence)
    (targetCardinality incidence)
    ( fmap
        (\entry -> mkBoundaryEntry (fromIntegral (sourceIndex entry)) (fromIntegral (targetIndex entry)) (f (boundaryCoefficient entry)))
        (boundaryEntries incidence)
    )

type BlockBoundaryEntry :: Type -> Type
data BlockBoundaryEntry r = BlockBoundaryEntry
  { blockSourceIndex :: Int,
    blockTargetIndex :: Int,
    blockSubmatrix :: BoundaryIncidence r
  }
  deriving stock (Eq, Show)

mkBlockBoundaryEntry :: Natural -> Natural -> BoundaryIncidence r -> BlockBoundaryEntry r
mkBlockBoundaryEntry blockSourceIndexValue blockTargetIndexValue submatrix =
  BlockBoundaryEntry
    { blockSourceIndex = fromIntegral blockSourceIndexValue,
      blockTargetIndex = fromIntegral blockTargetIndexValue,
      blockSubmatrix = submatrix
    }

type BlockBoundaryIncidence :: Type -> Type
data BlockBoundaryIncidence r = BlockBoundaryIncidence
  { blockSourceDimensions :: [Int],
    blockTargetDimensions :: [Int],
    blockEntries :: [BlockBoundaryEntry r]
  }
  deriving stock (Eq, Show)

mkBlockBoundaryIncidence :: [Natural] -> [Natural] -> [BlockBoundaryEntry r] -> Either BoundaryIncidenceShapeError (BlockBoundaryIncidence r)
mkBlockBoundaryIncidence sourceDimensionsValue targetDimensionsValue entries =
  let sourceDimensions = fmap fromIntegral sourceDimensionsValue
      targetDimensions = fmap fromIntegral targetDimensionsValue
      blockIncidence =
        BlockBoundaryIncidence
          { blockSourceDimensions = sourceDimensions,
            blockTargetDimensions = targetDimensions,
            blockEntries = entries
          }
   in do
        _ <- traverse (validateBlockEntry blockIncidence) entries
        pure blockIncidence

flattenBlockIncidence :: (Eq r, Semiring r) => BlockBoundaryIncidence r -> Either BoundaryIncidenceShapeError (BoundaryIncidence r)
flattenBlockIncidence block =
  let sourceOffsets = prefixSums (blockSourceDimensions block)
      targetOffsets = prefixSums (blockTargetDimensions block)
      totalSourceDim = sum (blockSourceDimensions block)
      totalTargetDim = sum (blockTargetDimensions block)
   in do
        expandedEntries <-
          fmap concat
            (traverse (expandBlockEntry sourceOffsets targetOffsets block) (blockEntries block))
        mkBoundaryIncidence
          (fromIntegral totalSourceDim)
          (fromIntegral totalTargetDim)
          expandedEntries

scaleBoundaryIncidence :: Num r => r -> BoundaryIncidence r -> BoundaryIncidence r
scaleBoundaryIncidence scalar =
  mapBoundaryCoefficients (* scalar)

prefixSums :: [Int] -> [Int]
prefixSums = scanl (+) 0

materializeIncidenceBoundary ::
  (Eq r, Semiring r, Ord target) =>
  (source -> [(r, target)]) ->
  [source] ->
  [target] ->
  Either HomologyFailure (BoundaryIncidence r)
materializeIncidenceBoundary boundaryOf sourceBasis targetBasis =
  materializeBoundary
    (\sourceValue -> boundaryOf sourceValue & fmap (\(coefficientValue, targetValue) -> (coefficientValue, ScopedBoundary IncidenceScope targetValue)))
    sourceBasis
    targetBasis

materializeBoundary ::
  (Eq r, Semiring r, Ord target) =>
  (source -> [(r, ScopedBoundary target)]) ->
  [source] ->
  [target] ->
  Either HomologyFailure (BoundaryIncidence r)
materializeBoundary boundaryOf sourceBasis targetBasis =
  -- The user boundary function is evaluated exactly once per basis element;
  -- the portal-scope gate and the materialization both read the same
  -- precomputed list rather than each paying for a full traversal.
  let boundariesBySource = fmap boundaryOf sourceBasis
      hasPortalTargets =
        boundariesBySource
          & concat
          & any (\(_, scopedTarget) -> boundaryScope scopedTarget == PortalScope)
   in if hasPortalTargets
        then Left (LawViolation IncidenceScopeLaw)
        else materializeIncidence boundariesBySource targetBasis

materializeIncidence ::
  (Eq r, Semiring r, Ord target) =>
  [[(r, ScopedBoundary target)]] ->
  [target] ->
  Either HomologyFailure (BoundaryIncidence r)
materializeIncidence boundariesBySource targetBasis =
  let targetIndexByBasis = Map.fromList (zip targetBasis [0 :: Int ..])
   in do
        entries <-
          fmap concat $
            traverse
              ( \(sourceIndexValue, sourceBoundary) ->
                  traverse
                    ( \(coefficientValue, scopedTarget) ->
                        maybe
                          (Left (InvalidBoundaryIncidence "boundary target is absent from the target basis"))
                          ( \targetIndexValue ->
                              Right
                                ( mkBoundaryEntry
                                    (fromIntegral sourceIndexValue)
                                    (fromIntegral targetIndexValue)
                                    coefficientValue
                                )
                          )
                          (Map.lookup (boundaryValue scopedTarget) targetIndexByBasis)
                    )
                    sourceBoundary
              )
              (zip [0 :: Int ..] boundariesBySource)
        either
          (Left . InvalidBoundaryIncidence . show)
          Right
          ( mkBoundaryIncidence
              (fromIntegral (length boundariesBySource))
              (fromIntegral (length targetBasis))
              entries
          )

uncheckedBoundaryIncidence :: Int -> Int -> [BoundaryEntry r] -> BoundaryIncidence r
uncheckedBoundaryIncidence sourceDimension targetDimension entries =
  BoundaryIncidence
    { sourceCardinality = sourceDimension,
      targetCardinality = targetDimension,
      boundaryEntries = entries
    }

firstOutOfBoundsEntry :: Int -> Int -> [BoundaryEntry r] -> Maybe (BoundaryEntry r)
firstOutOfBoundsEntry sourceDimension targetDimension =
  listToMaybe . filter (not . entryWithinBounds sourceDimension targetDimension)

entryWithinBounds :: Int -> Int -> BoundaryEntry r -> Bool
entryWithinBounds sourceDimension targetDimension entry =
  sourceIndex entry >= 0
    && sourceIndex entry < sourceDimension
    && targetIndex entry >= 0
    && targetIndex entry < targetDimension

entryOutOfBoundsError :: Int -> Int -> BoundaryEntry r -> BoundaryIncidenceShapeError
entryOutOfBoundsError sourceDimension targetDimension entry =
  BoundaryIncidenceEntryOutOfBounds
    (sourceIndex entry)
    (targetIndex entry)
    sourceDimension
    targetDimension

mappingDimension :: (Int -> Maybe Natural) -> Int -> Int
mappingDimension reindex dimension =
  [0 .. dimension - 1]
    & fmap reindex
    & mapMaybe (fmap fromIntegral)
    & maximumMaybe
    & maybe 0 (+ 1)

maximumMaybe :: Ord a => [a] -> Maybe a
maximumMaybe =
  foldr
    ( \value ->
        Just
          . maybe value (max value)
    )
    Nothing

validateBlockEntry :: BlockBoundaryIncidence r -> BlockBoundaryEntry r -> Either BoundaryIncidenceShapeError (BlockBoundaryEntry r)
validateBlockEntry blockIncidence blockEntry = do
  expectedSourceDim <-
    dimensionAt
      (blockSourceDimensions blockIncidence)
      (blockSourceIndex blockEntry)
      (blockIndexError blockIncidence blockEntry)
  expectedTargetDim <-
    dimensionAt
      (blockTargetDimensions blockIncidence)
      (blockTargetIndex blockEntry)
      (blockIndexError blockIncidence blockEntry)
  let submatrix = blockSubmatrix blockEntry
  if sourceCardinality submatrix /= expectedSourceDim || targetCardinality submatrix /= expectedTargetDim
    then
      Left
        ( BoundaryIncidenceBlockShapeMismatch
            expectedSourceDim
            expectedTargetDim
            (sourceCardinality submatrix)
            (targetCardinality submatrix)
        )
    else pure blockEntry

blockIndexError :: BlockBoundaryIncidence r -> BlockBoundaryEntry r -> BoundaryIncidenceShapeError
blockIndexError blockIncidence blockEntry =
  BoundaryIncidenceEntryOutOfBounds
    (blockSourceIndex blockEntry)
    (blockTargetIndex blockEntry)
    (length (blockSourceDimensions blockIncidence))
    (length (blockTargetDimensions blockIncidence))

expandBlockEntry :: [Int] -> [Int] -> BlockBoundaryIncidence r -> BlockBoundaryEntry r -> Either BoundaryIncidenceShapeError [BoundaryEntry r]
expandBlockEntry sourceOffsets targetOffsets blockIncidence blockEntry = do
  _ <- validateBlockEntry blockIncidence blockEntry
  sourceOffset <- offsetAt sourceOffsets (blockSourceIndex blockEntry) (blockIndexError blockIncidence blockEntry)
  targetOffset <- offsetAt targetOffsets (blockTargetIndex blockEntry) (blockIndexError blockIncidence blockEntry)
  traverse (expandScalarEntry sourceOffset targetOffset (blockSubmatrix blockEntry)) (boundaryEntries (blockSubmatrix blockEntry))

dimensionAt :: [Int] -> Int -> errorValue -> Either errorValue Int
dimensionAt dimensions idx errorValue
  | idx < 0 = Left errorValue
  | otherwise =
      case drop idx dimensions of
        dimensionValue : _ -> Right dimensionValue
        [] -> Left errorValue

offsetAt :: [Int] -> Int -> errorValue -> Either errorValue Int
offsetAt offsets idx errorValue
  | idx < 0 = Left errorValue
  | otherwise =
      case drop idx offsets of
        offsetValue : remainingOffsets ->
          if null remainingOffsets
            then Left errorValue
            else Right offsetValue
        [] -> Left errorValue

expandScalarEntry :: Int -> Int -> BoundaryIncidence r -> BoundaryEntry r -> Either BoundaryIncidenceShapeError (BoundaryEntry r)
expandScalarEntry sourceOffset targetOffset submatrix scalarEntry =
  if entryWithinBounds (sourceCardinality submatrix) (targetCardinality submatrix) scalarEntry
    then
      Right
        ( mkBoundaryEntry
            (fromIntegral (sourceIndex scalarEntry + sourceOffset))
            (fromIntegral (targetIndex scalarEntry + targetOffset))
            (boundaryCoefficient scalarEntry)
        )
    else Left (entryOutOfBoundsError (sourceCardinality submatrix) (targetCardinality submatrix) scalarEntry)