packages feed

moonlight-delta-0.1.0.2: src-core/Moonlight/Delta/Frontier.hs

{-# LANGUAGE DerivingStrategies #-}

{-|
Antichain frontiers over partial orders: lower, upper, and two-dimensional
product variants, answering containment queries. @ProductFrontier2@
canonicalizes a two-dimensional progress skyline by removing dominated points.

> import Moonlight.Delta.Frontier qualified as Frontier
>
> progress :: Frontier.ProductFrontier2 Int Int
> progress = Frontier.mkProductFrontier2 [(0, 3), (1, 2), (2, 1), (3, 0)]
>
> covered :: Bool
> covered = Frontier.productFrontier2Contains (2, 2) progress

Use @mkFrontier@ or @mkUpperFrontier@ for a general @PartialOrder@.
-}
module Moonlight.Delta.Frontier
  ( Frontier,
    UpperFrontier,
    ProductFrontier2,
    emptyFrontier,
    emptyUpperFrontier,
    singletonFrontier,
    singletonUpperFrontier,
    mkFrontier,
    mkUpperFrontier,
    mkProductFrontier2,
    frontierPoints,
    upperFrontierPoints,
    productFrontier2Points,
    frontierNull,
    upperFrontierNull,
    frontierContains,
    productFrontier2Contains,
    insertPoint,
    insertUpperFrontierPoint,
  )
where

import Data.Bool (Bool (False, True))
import Data.Foldable qualified as Foldable
import Data.Kind (Type)
import Data.List qualified as List
import Data.Map.Strict
  ( Map,
  )
import Data.Map.Strict qualified as Map
import Moonlight.Core
  ( PartialOrder (..),
  )
import Prelude (Eq (..), Maybe (..), Ord (..), Show, flip, maybe, min, not, otherwise, reverse, (.))

type LowerFrontierPolarity :: Type
data LowerFrontierPolarity

type UpperFrontierPolarity :: Type
data UpperFrontierPolarity

type FrontierWith :: Type -> Type -> Type
newtype FrontierWith polarity time = Frontier
  { antichain :: [time]
  }
  deriving stock (Eq, Ord, Show)

type Frontier :: Type -> Type
type Frontier =
  FrontierWith LowerFrontierPolarity

type UpperFrontier :: Type -> Type
type UpperFrontier =
  FrontierWith UpperFrontierPolarity

type ProductFrontier2 :: Type -> Type -> Type
newtype ProductFrontier2 left right = ProductFrontier2
  { staircase :: Map left right
  }
  deriving stock (Eq, Ord, Show)

emptyFrontier :: Frontier time
emptyFrontier =
  Frontier []

emptyUpperFrontier :: UpperFrontier time
emptyUpperFrontier =
  Frontier []

singletonFrontier :: time -> Frontier time
singletonFrontier time =
  Frontier [time]

singletonUpperFrontier :: time -> UpperFrontier time
singletonUpperFrontier time =
  Frontier [time]

mkFrontier ::
  (Ord time, PartialOrder time) =>
  [time] ->
  Frontier time
mkFrontier times =
  canonicalizeFrontier
    (Foldable.foldl' (flip (insertFrontierRaw leq)) emptyFrontier times)

mkUpperFrontier ::
  (Ord time, PartialOrder time) =>
  [time] ->
  UpperFrontier time
mkUpperFrontier times =
  canonicalizeFrontier
    (Foldable.foldl' (flip (insertFrontierRaw (flip leq))) emptyUpperFrontier times)

mkProductFrontier2 ::
  (Ord left, Ord right) =>
  [(left, right)] ->
  ProductFrontier2 left right
mkProductFrontier2 =
  ProductFrontier2 . productFrontierStaircase . Map.fromListWith min

frontierPoints :: Frontier time -> [time]
frontierPoints =
  antichain

upperFrontierPoints :: UpperFrontier time -> [time]
upperFrontierPoints =
  antichain

productFrontier2Points :: ProductFrontier2 left right -> [(left, right)]
productFrontier2Points =
  Map.toAscList . staircase

frontierNull :: Frontier time -> Bool
frontierNull (Frontier times) =
  case times of
    [] -> True
    _ -> False

upperFrontierNull :: UpperFrontier time -> Bool
upperFrontierNull (Frontier times) =
  case times of
    [] -> True
    _ -> False

frontierContains ::
  PartialOrder time =>
  time ->
  Frontier time ->
  Bool
frontierContains time (Frontier times) =
  Foldable.any
    (`leq` time)
    times

productFrontier2Contains ::
  (Ord left, Ord right) =>
  (left, right) ->
  ProductFrontier2 left right ->
  Bool
productFrontier2Contains (left, right) (ProductFrontier2 staircase) =
  maybe
    False
    (\(_, frontierRight) -> frontierRight <= right)
    (Map.lookupLE left staircase)

insertPoint ::
  (Ord time, PartialOrder time) =>
  time ->
  Frontier time ->
  Frontier time
insertPoint =
  insertFrontierWith leq

insertUpperFrontierPoint ::
  (Ord time, PartialOrder time) =>
  time ->
  UpperFrontier time ->
  UpperFrontier time
insertUpperFrontierPoint =
  insertFrontierWith (flip leq)

insertFrontierWith ::
  Ord time =>
  (time -> time -> Bool) ->
  time ->
  FrontierWith polarity time ->
  FrontierWith polarity time
insertFrontierWith coveredBy time frontier@(Frontier times) =
  case times of
    [] ->
      Frontier [time]
    [existingTime]
      | existingTime `coveredBy` time ->
          frontier
      | time `coveredBy` existingTime ->
          Frontier [time]
      | otherwise ->
          Frontier (List.insert time [existingTime])
    _ ->
      if Foldable.any (`coveredBy` time) times
        then frontier
        else
          Frontier (List.insert time (List.filter (not . coveredBy time) times))

insertFrontierRaw ::
  (time -> time -> Bool) ->
  time ->
  FrontierWith polarity time ->
  FrontierWith polarity time
insertFrontierRaw coveredBy time frontier@(Frontier times) =
  case times of
    [] ->
      Frontier [time]
    _ ->
      if Foldable.any (`coveredBy` time) times
        then frontier
        else Frontier (time : List.filter (not . coveredBy time) times)

canonicalizeFrontier :: Ord time => FrontierWith polarity time -> FrontierWith polarity time
canonicalizeFrontier (Frontier times) =
  Frontier (List.sort times)

productFrontierStaircase ::
  Ord right =>
  Map left right ->
  Map left right
productFrontierStaircase candidates =
  Map.fromDistinctAscList (reverse keptEntries)
  where
    (_, keptEntries) =
      Map.foldlWithKey' retainUndominated (Nothing, []) candidates

    retainUndominated ::
      Ord right =>
      (Maybe right, [(left, right)]) ->
      left ->
      right ->
      (Maybe right, [(left, right)])
    retainUndominated (bestRight, kept) left right =
      case bestRight of
        Nothing ->
          (Just right, (left, right) : kept)
        Just currentRight
          | right < currentRight ->
              (Just right, (left, right) : kept)
        _ ->
          (bestRight, kept)