packages feed

moonlight-core-0.1.0.0: src-numeric/Moonlight/Core/Niche/Internal.hs

module Moonlight.Core.Niche.Internal
  ( NicheValidationError (..),
    TopoSample,
    mkTopoSample,
    flatTopoSample,
    topoSlope,
    topoAspect,
    topoCurvature,
    StressorId,
    mkStressorId,
    unsafeTrustStressorId,
    renderStressorId,
    defaultStressorId,
    ActiveStressor,
    mkActiveStressor,
    activeStressorId,
    activeStressorIntensity,
    ActiveStressorSet,
    emptyActiveStressorSet,
    activeStressorSetFromList,
    activeStressorSetEntries,
    activeStressorSetTopEntries,
    ContextSignature,
    emptyContextSignature,
    mkContextSignature,
    contextSignatureBins,
  )
where

import Data.Bifunctor (first)
import Data.Kind (Type)
import Data.List (genericTake, sortBy)
import qualified Data.Map.Strict as Map
import Data.Ord (Down (..), comparing)
import Data.Text (Text, pack)
import Data.Text qualified as Text
import Data.Word (Word32)
import Moonlight.Core.Canon (canonicalize)
import Moonlight.Core.Error (MoonlightError)
import Moonlight.Core.Identifier
  ( IdentifierToken,
    mkScopedIdentifier,
    renderIdentifierToken,
  )
import Moonlight.Internal.Unsound (TrustJustification, unsafelyTrustIdentifierToken)
import Numeric.Natural (Natural)
import Prelude
  ( Bool (..),
    Double,
    Either (..),
    Eq,
    Maybe (..),
    Ord,
    Show,
    compare,
    map,
    max,
    otherwise,
    pure,
    uncurry,
    (.),
    (==),
    (>),
    (>=),
  )

type NicheValidationError :: Type
data NicheValidationError
  = NicheCanonicalScalarRejected !MoonlightError
  | NegativeTopoSlope !Double
  | NonPositiveStressorIntensity !Double
  deriving stock (Eq, Show)

type TopoSample :: Type
data TopoSample = TopoSample
  { topoSlope :: Double,
    topoAspect :: Double,
    topoCurvature :: Double
  }
  deriving stock (Eq, Show)

mkTopoSample :: Double -> Double -> Double -> Either NicheValidationError TopoSample
mkTopoSample slopeValue aspectValue curvatureValue = do
  canonicalSlope <- canonicalizeScalar slopeValue
  canonicalAspect <- canonicalizeScalar aspectValue
  canonicalCurvature <- canonicalizeScalar curvatureValue
  if canonicalSlope >= 0.0
    then pure (TopoSample canonicalSlope canonicalAspect canonicalCurvature)
    else Left (NegativeTopoSlope canonicalSlope)

flatTopoSample :: TopoSample
flatTopoSample = TopoSample 0.0 0.0 0.0

type StressorIdNamespace :: Type
data StressorIdNamespace

type StressorId :: Type
data StressorId
  = DefaultStressorId
  | StressorId !(IdentifierToken StressorIdNamespace)
  deriving stock (Show)

instance Eq StressorId where
  left == right =
    case (left, right) of
      (DefaultStressorId, DefaultStressorId) ->
        True
      (StressorId leftToken, StressorId rightToken) ->
        leftToken == rightToken
      _ ->
        renderStressorId left == renderStressorId right

instance Ord StressorId where
  compare left right =
    case (left, right) of
      (DefaultStressorId, DefaultStressorId) ->
        compare (renderStressorId left) (renderStressorId right)
      (StressorId leftToken, StressorId rightToken) ->
        compare leftToken rightToken
      _ ->
        compare (renderStressorId left) (renderStressorId right)

mkStressorId :: Text -> Maybe StressorId
mkStressorId rawInput
  | Text.strip rawInput == defaultStressorIdText =
      Just DefaultStressorId
  | otherwise =
      mkScopedIdentifier StressorId rawInput

unsafeTrustStressorId :: TrustJustification -> Text -> StressorId
unsafeTrustStressorId justification rawInput
  | Text.strip rawInput == defaultStressorIdText =
      DefaultStressorId
  | otherwise =
      StressorId (unsafelyTrustIdentifierToken justification rawInput)

renderStressorId :: StressorId -> Text
renderStressorId stressorId =
  case stressorId of
    DefaultStressorId ->
      defaultStressorIdText
    StressorId identifierToken ->
      renderIdentifierToken identifierToken

defaultStressorId :: StressorId
defaultStressorId =
  defaultStressorIdFrom (mkStressorId defaultStressorIdText)

defaultStressorIdText :: Text
defaultStressorIdText =
  pack "default"

defaultStressorIdFrom :: Maybe StressorId -> StressorId
defaultStressorIdFrom maybeStressorId =
  case maybeStressorId of
    Just stressorId -> stressorId
    Nothing -> DefaultStressorId

type ActiveStressor :: Type
data ActiveStressor = ActiveStressor
  { activeStressorId :: StressorId,
    activeStressorIntensity :: Double
  }
  deriving stock (Eq, Show)

mkActiveStressor :: StressorId -> Double -> Either NicheValidationError ActiveStressor
mkActiveStressor stressorId intensityValue = do
  canonicalIntensity <- canonicalizeScalar intensityValue
  if canonicalIntensity > 0.0
    then pure (ActiveStressor stressorId canonicalIntensity)
    else Left (NonPositiveStressorIntensity canonicalIntensity)

type ActiveStressorSet :: Type
newtype ActiveStressorSet = ActiveStressorSet [ActiveStressor]
  deriving stock (Eq, Show)

emptyActiveStressorSet :: ActiveStressorSet
emptyActiveStressorSet = ActiveStressorSet []

activeStressorSetFromList :: [ActiveStressor] -> ActiveStressorSet
activeStressorSetFromList stressors =
  ActiveStressorSet
    ( sortBy canonicalOrder
        ( map (uncurry ActiveStressor)
            (Map.toList (Map.fromListWith max (map stressorEntry stressors)))
        )
    )
  where
    stressorEntry stressor =
      (activeStressorId stressor, activeStressorIntensity stressor)
    canonicalOrder =
      comparing
        (\stressor -> (Down (activeStressorIntensity stressor), activeStressorId stressor))

activeStressorSetEntries :: ActiveStressorSet -> [ActiveStressor]
activeStressorSetEntries (ActiveStressorSet stressors) = stressors

activeStressorSetTopEntries :: Natural -> ActiveStressorSet -> [ActiveStressor]
activeStressorSetTopEntries limit =
  genericTake limit . activeStressorSetEntries

type ContextSignature :: Type
newtype ContextSignature = ContextSignature [Word32]
  deriving stock (Eq, Ord, Show)

emptyContextSignature :: ContextSignature
emptyContextSignature = ContextSignature []

mkContextSignature :: [Word32] -> ContextSignature
mkContextSignature = ContextSignature

contextSignatureBins :: ContextSignature -> [Word32]
contextSignatureBins (ContextSignature bins) = bins

canonicalizeScalar :: Double -> Either NicheValidationError Double
canonicalizeScalar = first NicheCanonicalScalarRejected . canonicalize