packages feed

moonlight-linalg-0.1.0.0: src-statics/Moonlight/LinAlg/Pure/Statics/Core.hs

{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

module Moonlight.LinAlg.Pure.Statics.Core
  ( checkEquilibrium,
    solveGraphicStatics,
  )
where

import Control.Monad (join)
import Data.Graph (SCC (..), stronglyConnComp)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NonEmpty
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Set (Set)
import qualified Data.Set as Set
import Moonlight.Core (MoonlightError (..))
import Moonlight.LinAlg.Pure.Dense.Rows (transposeRowsExact)
import Moonlight.LinAlg.Pure.Dense.Dynamic
  ( DynMatrix,
    DynVector,
    dynMatrixToRows,
    dynMatrixShape,
    dynVectorLength,
    dynVectorToList,
    fromDynMatrix,
    fromDynVector,
    mkDynMatrix,
    mkDynVector,
    toDynVector,
    withDynMatrix,
    withDynVector,
  )
import Moonlight.LinAlg.Internal.Primitives (matrixVectorProduct)
import Moonlight.LinAlg.Pure.Dense.Decomposition (qrDecompFullColumnRank)
import Moonlight.LinAlg.Pure.Dense.Solver (solveDirect)
import Moonlight.LinAlg.Pure.Statics.Algebra
  ( addVec3,
    axisVector,
    magnitudeVec3,
    memberEndpoints,
    memberTouchesNode,
    vec3Zero,
  )
import Moonlight.LinAlg.Pure.Statics.Compile (assembleEquilibriumEquations)
import Moonlight.LinAlg.Pure.Statics.Types
  ( CompiledEquilibrium,
    EquationRef (..),
    EquilibriumResult (..),
    EquilibriumSolution (..),
    EquilibriumViolation (..),
    ForceNetwork,
    ForceSign (..),
    MemberRef,
    NodeRef,
    UnknownForce (..),
    Vec3,
    compiledCoefficientMatrix,
    compiledEquationOrder,
    compiledFoundationOrder,
    compiledMemberOrder,
    compiledNodeOrder,
    compiledRightHandSide,
    compiledUnknownOrder,
  )
import Moonlight.LinAlg.Pure.Dense.Types
  ( Matrix,
    Vector,
    fromListVector,
    matrixToRows,
    toListVector,
  )
import Prelude

checkEquilibrium :: ForceNetwork -> Either MoonlightError EquilibriumResult
checkEquilibrium networkValue =
  assembleEquilibriumEquations networkValue >>= solveGraphicStatics

solveGraphicStatics :: CompiledEquilibrium -> Either MoonlightError EquilibriumResult
solveGraphicStatics compiledValue = do
  solvedUnknowns <- solveUnknowns compiledValue
  solutionValues <- pure (dynVectorToList solvedUnknowns)
  residualForces <- solveResiduals compiledValue solutionValues
  let solutionValue = interpretSolution compiledValue solutionValues residualForces
      violations = collectViolations compiledValue solutionValue
  pure
    ( maybe
        (InEquilibrium solutionValue)
        Disequilibrium
        violations
    )

solveUnknowns :: CompiledEquilibrium -> Either MoonlightError (DynVector Double)
solveUnknowns compiledValue =
  case componentNodeSets compiledValue of
    [] -> solveUnknownsDense compiledValue
    [_] -> solveUnknownsDense compiledValue
    components -> solveUnknownsByComponents components compiledValue

solveUnknownsDense :: CompiledEquilibrium -> Either MoonlightError (DynVector Double)
solveUnknownsDense compiledValue =
  solveDenseSystemByShape
    (compiledCoefficientMatrix compiledValue)
    (compiledRightHandSide compiledValue)

solveUnknownsByComponents :: [Set NodeRef] -> CompiledEquilibrium -> Either MoonlightError (DynVector Double)
solveUnknownsByComponents components compiledValue = do
  coefficientRows <- dynMatrixToRows (compiledCoefficientMatrix compiledValue)
  solvedEntries <-
    fmap concat
      ( traverse
          (solveComponentUnknowns compiledValue coefficientRows (dynVectorToList (compiledRightHandSide compiledValue)))
          components
      )
  let solvedMap = Map.fromList solvedEntries
      unknownCount = length (compiledUnknownOrder compiledValue)
  solutionValues <-
    traverse
      ( \unknownIndex ->
          maybe
            (Left (InvariantViolation ("graphic statics component solve omitted unknown index " <> show unknownIndex)))
            Right
            (Map.lookup unknownIndex solvedMap)
      )
      [0 .. unknownCount - 1]
  mkDynVector unknownCount solutionValues

solveComponentUnknowns ::
  CompiledEquilibrium ->
  [[Double]] ->
  [Double] ->
  Set NodeRef ->
  Either MoonlightError [(Int, Double)]
solveComponentUnknowns compiledValue coefficientRows rightHandSideValues componentNodes = do
  let equationEntries =
        filter
          ( \(_, equationRefValue) ->
              Set.member (equationNodeRef equationRefValue) componentNodes
          )
          (indexedValues (compiledEquationOrder compiledValue))
      unknownEntries =
        filter
          (unknownEntryInComponent componentNodes)
          (indexedValues (compiledUnknownOrder compiledValue))
  componentRows <-
    traverse
      ( \(equationIndex, _) -> do
          rowValues <- selectIndex "graphic statics component equation row" equationIndex coefficientRows
          traverse
            (\(unknownIndex, _) -> selectIndex "graphic statics component unknown column" unknownIndex rowValues)
            unknownEntries
      )
      equationEntries
  componentRightHandSide <-
    traverse
      (\(equationIndex, _) -> selectIndex "graphic statics component RHS" equationIndex rightHandSideValues)
      equationEntries
  componentMatrix <- mkDynMatrix (length equationEntries) (length unknownEntries) (concat componentRows)
  componentVector <- mkDynVector (length equationEntries) componentRightHandSide
  componentSolution <- solveComponentDenseSystem componentMatrix componentVector
  let solutionValues = dynVectorToList componentSolution
  if length solutionValues /= length unknownEntries
    then Left (InvariantViolation "graphic statics component solve returned wrong unknown count")
    else Right (zip (fst <$> unknownEntries) solutionValues)

solveComponentDenseSystem :: DynMatrix Double -> DynVector Double -> Either MoonlightError (DynVector Double)
solveComponentDenseSystem =
  solveDenseSystemByShape

solveDenseSystemByShape :: DynMatrix Double -> DynVector Double -> Either MoonlightError (DynVector Double)
solveDenseSystemByShape coefficientMatrix rightHandSide =
  let (rowCount, columnCount) = dynMatrixShape coefficientMatrix
   in if rowCount == columnCount && rowCount == dynVectorLength rightHandSide
        then solveSquareSystem coefficientMatrix rightHandSide
        else solveLeastSquares coefficientMatrix rightHandSide

componentNodeSets :: CompiledEquilibrium -> [Set NodeRef]
componentNodeSets compiledValue =
  Set.fromList . flattenSCC
    <$> stronglyConnComp
      ( (\nodeRefValue -> (nodeRefValue, nodeRefValue, Map.findWithDefault [] nodeRefValue adjacencyMap))
          <$> compiledNodeOrder compiledValue
      )
  where
    adjacencyMap =
      Map.fromListWith
        (<>)
        (componentMemberAdjacency =<< compiledMemberOrder compiledValue)

componentMemberAdjacency :: MemberRef -> [(NodeRef, [NodeRef])]
componentMemberAdjacency memberRefValue =
  case memberEndpoints memberRefValue of
    (leftRef, rightRef) ->
      [ (leftRef, [rightRef]),
        (rightRef, [leftRef])
      ]

flattenSCC :: SCC node -> [node]
flattenSCC component =
  case component of
    AcyclicSCC nodeValue -> [nodeValue]
    CyclicSCC nodeValues -> nodeValues

unknownEntryInComponent :: Set NodeRef -> (Int, UnknownForce) -> Bool
unknownEntryInComponent componentNodes (_, unknownValue) =
  case unknownValue of
    MemberUnknown memberRefValue ->
      case memberEndpoints memberRefValue of
        (leftRef, rightRef) ->
          Set.member leftRef componentNodes || Set.member rightRef componentNodes
    ReactionUnknown nodeRefValue _ ->
      Set.member nodeRefValue componentNodes

indexedValues :: [value] -> [(Int, value)]
indexedValues =
  zip [0 ..]

selectIndex :: String -> Int -> [value] -> Either MoonlightError value
selectIndex context indexValue values
  | indexValue < 0 =
      Left (InvariantViolation (context <> " index must be non-negative: " <> show indexValue))
  | otherwise =
      case drop indexValue values of
        value : _ -> Right value
        [] ->
          Left
            ( InvariantViolation
                ( context
                    <> " index out of bounds: index="
                    <> show indexValue
                    <> ", length="
                    <> show (length values)
                )
            )

solveSquareSystem :: DynMatrix Double -> DynVector Double -> Either MoonlightError (DynVector Double)
solveSquareSystem coefficientMatrix rightHandSide
  | rowCount /= columnCount =
      Left (InvariantViolation "graphic statics direct solve requires a square coefficient matrix")
  | rowCount /= dynVectorLength rightHandSide =
      Left (InvariantViolation "graphic statics direct solve RHS length mismatch")
  | otherwise =
      join
        ( withDynVector rightHandSide
            ( \(staticRightHandSide :: Vector n Double) -> do
                staticMatrix <- (fromDynMatrix coefficientMatrix :: Either MoonlightError (Matrix n n Double))
                toDynVector <$> solveDirect staticMatrix staticRightHandSide
            )
        )
  where
    (rowCount, columnCount) = dynMatrixShape coefficientMatrix

solveLeastSquares :: DynMatrix Double -> DynVector Double -> Either MoonlightError (DynVector Double)
solveLeastSquares coefficientMatrix rightHandSide
  | rowCount /= dynVectorLength rightHandSide =
      Left (InvariantViolation "graphic statics least-squares RHS length mismatch")
  | rowCount < columnCount =
      Left (InvariantViolation "graphic statics QR least-squares requires row count greater than or equal to unknown count")
  | otherwise =
      join
        ( withDynMatrix coefficientMatrix
            ( \(staticMatrix :: Matrix rows columns Double) -> do
                staticRightHandSide <- (fromDynVector rightHandSide :: Either MoonlightError (Vector rows Double))
                (qMatrix, rMatrix) <- qrDecompFullColumnRank staticMatrix
                qRows <- matrixToRows qMatrix
                qTransposeRows <- transposeRowsExact qRows
                projectedRightHandSideValues <- matrixVectorProduct qTransposeRows (toListVector staticRightHandSide)
                projectedRightHandSide <- fromListVector @columns projectedRightHandSideValues
                toDynVector <$> solveDirect rMatrix projectedRightHandSide
            )
        )
  where
    (rowCount, columnCount) = dynMatrixShape coefficientMatrix

solveResiduals :: CompiledEquilibrium -> [Double] -> Either MoonlightError (Map NodeRef Vec3)
solveResiduals compiledValue solvedUnknowns = do
  coefficientRows <- dynMatrixToRows (compiledCoefficientMatrix compiledValue)
  let rightHandSideValues = dynVectorToList (compiledRightHandSide compiledValue)
  predictedValues <- matrixVectorProduct coefficientRows solvedUnknowns
  if length predictedValues /= length rightHandSideValues
    then Left (InvariantViolation "graphic statics residual computation length mismatch")
    else
      pure
        ( foldl'
            accumulateResidual
            Map.empty
            ( zip
                (compiledEquationOrder compiledValue)
                (zipWith (-) predictedValues rightHandSideValues)
            )
        )

interpretSolution :: CompiledEquilibrium -> [Double] -> Map NodeRef Vec3 -> EquilibriumSolution
interpretSolution compiledValue solvedUnknowns residualForces =
  let solutionEntries = zip (compiledUnknownOrder compiledValue) solvedUnknowns
      (memberForces, reactionForces) =
        foldl'
          accumulateUnknown
          (Map.empty, Map.empty)
          solutionEntries
   in EquilibriumSolution
        { equilibriumMemberForces = memberForces,
          equilibriumReactionForces =
            foldl'
              (\reactionMap nodeRefValue -> Map.insertWith addVec3 nodeRefValue vec3Zero reactionMap)
              reactionForces
              (compiledFoundationOrder compiledValue),
          equilibriumResidualForces =
            foldl'
              (\residualMap nodeRefValue -> Map.insertWith addVec3 nodeRefValue vec3Zero residualMap)
              residualForces
              (compiledNodeOrder compiledValue)
        }

collectViolations :: CompiledEquilibrium -> EquilibriumSolution -> Maybe (NonEmpty EquilibriumViolation)
collectViolations compiledValue solutionValue =
  NonEmpty.nonEmpty
    ( foldMap
        (violationAtNode compiledValue solutionValue)
        (compiledNodeOrder compiledValue)
    )

violationAtNode :: CompiledEquilibrium -> EquilibriumSolution -> NodeRef -> [EquilibriumViolation]
violationAtNode compiledValue solutionValue nodeRefValue =
  let residualForce =
        Map.findWithDefault vec3Zero nodeRefValue (equilibriumResidualForces solutionValue)
      residualMagnitude = magnitudeVec3 residualForce
      memberDetails = incidentMembers compiledValue solutionValue nodeRefValue
      worstMember = strongestMember memberDetails
      tensionMember = strongestTension memberDetails
      selectedMember = maybe worstMember Just tensionMember
      selectedSign =
        fmap
          (\(_, forceValue) -> if forceValue < 0.0 then Tension else Compression)
          selectedMember
   in if residualMagnitude > equilibriumTolerance || tensionMember /= Nothing
        then
          [ EquilibriumViolation
              { violationNode = nodeRefValue,
                violationResidualForce = residualForce,
                violationResidualMagnitude = residualMagnitude,
                violationWorstMember = fmap fst selectedMember,
                violationMemberForceSign = selectedSign
              }
          ]
        else []

incidentMembers :: CompiledEquilibrium -> EquilibriumSolution -> NodeRef -> [(MemberRef, Double)]
incidentMembers compiledValue solutionValue nodeRefValue =
  fmap
    (\memberRefValue -> (memberRefValue, Map.findWithDefault 0.0 memberRefValue (equilibriumMemberForces solutionValue)))
    ( filter
        (memberTouchesNode nodeRefValue)
        (compiledMemberOrder compiledValue)
    )

strongestMember :: [(MemberRef, Double)] -> Maybe (MemberRef, Double)
strongestMember =
  foldl'
    ( \currentBest candidate ->
        case currentBest of
          Nothing -> Just candidate
          Just bestCandidate ->
            if abs (snd candidate) > abs (snd bestCandidate)
              then Just candidate
              else currentBest
    )
    Nothing

strongestTension :: [(MemberRef, Double)] -> Maybe (MemberRef, Double)
strongestTension =
  foldl'
    ( \currentBest candidate ->
        if snd candidate < (-equilibriumTolerance)
          then
            case currentBest of
              Nothing -> Just candidate
              Just bestCandidate ->
                if snd candidate < snd bestCandidate
                  then Just candidate
                  else currentBest
          else currentBest
    )
    Nothing

accumulateUnknown ::
  (Map MemberRef Double, Map NodeRef Vec3) ->
  (UnknownForce, Double) ->
  (Map MemberRef Double, Map NodeRef Vec3)
accumulateUnknown (memberForces, reactionForces) (unknownValue, magnitudeValue) =
  case unknownValue of
    MemberUnknown memberRefValue ->
      (Map.insert memberRefValue magnitudeValue memberForces, reactionForces)
    ReactionUnknown nodeRefValue axisValue ->
      ( memberForces,
        Map.insertWith addVec3 nodeRefValue (axisVector axisValue magnitudeValue) reactionForces
      )

accumulateResidual :: Map NodeRef Vec3 -> (EquationRef, Double) -> Map NodeRef Vec3
accumulateResidual residuals (equationRefValue, magnitudeValue) =
  Map.insertWith
    addVec3
    (equationNodeRef equationRefValue)
    (axisVector (equationAxis equationRefValue) magnitudeValue)
    residuals

equilibriumTolerance :: Double
equilibriumTolerance = 1.0e-8