packages feed

deltaq-1.2.0.0: src/DeltaQ/Expr.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -Wno-unused-top-binds #-}

{-|
Copyright   :
    Predictable Network Solutions Ltd., 2020-2024
    PLWORKZ R&D, 2025
License     : BSD-3-Clause
Description : Outcome expressions.

The type 'O' represents outcome expressions that can be

* inspected,
* rendered as outcome diagrams, and
* converted to a probability distribution of completion times.

-}
module DeltaQ.Expr
    ( -- * Outcome expressions
      O
    , var
    , loc
    , substitute
    , toDeltaQ

    -- * Outcome terms
    , termFromOutcome
    , outcomeFromTerm
    , Term (..)
    , isVar
    , isLoc
    , isSeq
    , isLast
    , isFirst
    , isParallel
    , isChoices
    , maxParallel
    , everywhere
    , everything
    , isNormalizedAssoc
    , normalizeAssoc
    ) where

import Control.Monad
    ( ap
    )
import Control.DeepSeq
    ( NFData
    )
import Data.List
    ( foldl'
    )
import DeltaQ.Class
    ( Outcome (..)
    , DeltaQ (..)
    , ProbabilisticOutcome (..)
    )
import DeltaQ.PiecewisePolynomial
    ( DQ
    )
import GHC.Generics
    ( Generic
    )

{-----------------------------------------------------------------------------
    O
------------------------------------------------------------------------------}
-- | Outcome expression.
--
-- This type includes graphical annotations for the rendering
-- as outcome diagram.
newtype O = O { unO :: Term String }
    -- INVARIANT: Terms satisfy 'isNormalizedAssoc'.
    deriving (Show, Generic, NFData)

-- | Outcome variable, given by a unique name.
var :: String -> O
var = O . Var

-- | Observation location, with a label.
--
-- Rendered as a box with a label in the outcome diagram.
-- Put this in sequence with other outcome expressions to highlight
-- their observation locations.
loc :: String -> O
loc = O . Loc

-- | Substitute all variables by outcome expressions.
substitute
    :: (String -> O)
        -- ^ Assignment of variable names to outcome expressions.
    -> O
        -- ^ Outcome expression in which we substitute.
    -> O
substitute f (O term) = O $ normalize1Assoc $ term >>= unO . f

-- | Map an outcome expression 'O' to
-- probability distributions of completion times 'DQ',
-- given a basic mapping from 'var' names to completion times.
toDeltaQ :: (String -> DQ) -> O -> DQ
toDeltaQ f (O term) = go term
  where
    go (Var v)  = f v
    go Never    = never
    go Wait0    = wait 0
    go (Wait t) = wait t
    go (Uniform tl tr) = uniform tl tr
    go (Loc  _) = wait 0
    go (Seq   xs) = foldr1 (.>>.) $ map go xs
    go (Last  xs) = foldr1 (./\.) $ map go xs
    go (First xs) = foldr1 (.\/.) $ map go xs
    go (Choices wxs) = choices [ (w, go x) | (w, x) <- wxs ]

-- | Outcome expressions can be combined sequentially and in parallel.
instance Outcome O where
    type Duration O = Rational

    never = O Never
    wait = O . Wait
    sequentially  (O x) (O y) = O . normalize1Assoc $ Seq [x,y]
    firstToFinish (O x) (O y) = O . normalize1Assoc $ First [x,y]
    lastToFinish  (O x) (O y) = O . normalize1Assoc $ Last [x,y]

-- | Outcome expressions can be combined with random choice.
instance ProbabilisticOutcome O where
    type Probability O = Rational

    choice p (O x) (O y) = O $ Choices [(p, x), (1-p, y)]
    choices wos = O $ Choices [ (w, x) | (w, O x) <- wos ]

{-----------------------------------------------------------------------------
    Terms
------------------------------------------------------------------------------}
-- | Term representation for outcome expressions.
--
-- Different terms may represent equal outcomes.
data Term v
    = Var v
        -- ^ Variable, to be substituted by a more concrete outcome.
    | Never
        -- ^ Outcome that never finishes.
    | Wait0
        -- ^ Succeed immediately. Equivalent to @Wait 0@,
        -- but with a straight line as graphical representation.
    | Wait Rational
        -- ^ Succeed after waiting for a fixed amount of time.
    | Uniform Rational Rational
        -- ^ @Uniform l r@ succeeds after an amount of time randomly drawn
        -- from a uniform probability distribution on the interval $[l,r]$.
    | Loc String
        -- ^ Outcome location with a label.
        -- Equivalent to @Wait 0@, but with a labeled box
        -- as graphical representation.
    | Seq [Term v]
        -- ^ Sequential composition.
    | Last [Term v]
        -- ^ Parallel composition, last to finish.
    | First [Term v]
        -- ^ Parallel composiiton, first to finish.
    | Choices [(Rational, Term v)]
        -- ^ Probabilistic choice.
        -- The probabilities are proportional to the given weights.
    deriving (Show, Eq, Ord, Generic, Functor, NFData)

instance Applicative Term where
    pure = Var
    (<*>) = ap

-- | '(>>=)' is substitution.
instance Monad Term where
    m >>= g = go m
      where
        go (Var v)    = g v
        go Never      = Never
        go Wait0      = Wait0
        go (Wait t)   = Wait t
        go (Uniform tl tr) = Uniform tl tr
        go (Loc  s)   = Loc s
        go (Seq   xs) = Seq   $ map go xs
        go (Last  xs) = Last  $ map go xs
        go (First xs) = First $ map go xs
        go (Choices wxs) = Choices [ (w, go x) | (w, x) <- wxs ]

-- | Inspect an outcome expression 'O' through its 'Term' representation.
--
-- The result 'Term' satisfies 'isNormalizedAssoc'.
termFromOutcome :: O -> Term String
termFromOutcome (O term) = term

-- | Construct an outcome expression 'O' from an outcome 'Term'.
outcomeFromTerm :: Term String -> O
outcomeFromTerm = O . normalizeAssoc

-- | Predicate that defines the \"associative normal form\" for a 'Term'.
--
-- Specifically, a 'Term' is said to be in \"associative normal form\"
-- if the arguments to the constructors 'Seq', 'Last', and 'First'
--
-- * are nonempty lists, and
-- * their list elements do not have an outermost constructor of the same
--   kind, i.e. @Seq [Seq …, First …]@ is not allowed because
--   one of the list elements for a 'Seq' constructor
--   is also a 'Seq' constructor.
--
-- and if the argument to the constructor 'Choices'
--
-- * is a nonempty list
--
isNormalizedAssoc ::Term v -> Bool 
isNormalizedAssoc (Seq xs) =
    not (null xs)
    && all (not . isSeq) xs
    && all isNormalizedAssoc xs
isNormalizedAssoc (Last xs) =
    not (null xs)
    && all (not . isLast) xs
    && all isNormalizedAssoc xs
isNormalizedAssoc (First xs) =
    not (null xs)
    && all (not . isFirst) xs
    && all isNormalizedAssoc xs
isNormalizedAssoc (Choices wxs) =
    not (null wxs)
    && all isNormalizedAssoc (map snd wxs)
isNormalizedAssoc _ =
    True

-- | Check whether a 'Term' is a 'Var'.
isVar :: Term v -> Bool
isVar (Var _) = True
isVar _       = False

-- | Check whether a 'Term' is a 'Loc'.
isLoc :: Term v -> Bool
isLoc (Loc _) = True
isLoc _       = False

-- | Check whether a 'Term' is a 'Seq'.
isSeq :: Term v -> Bool
isSeq (Seq _) = True
isSeq _ = False

-- | Check whether a 'Term' is a 'Last'.
isLast :: Term v -> Bool
isLast (Last _) = True
isLast _ = False

-- | Check whether a 'Term' is a 'First'.
isFirst :: Term v -> Bool
isFirst (First _) = True
isFirst _ = False

-- | Check whether a 'Term' is a 'Choices'.
isChoices :: Term v -> Bool
isChoices (Choices _) = True
isChoices _ = False

-- | Check whether a 'Term' is a parallel operation,
-- i.e. 'Last' or 'First'.
isParallel :: Term v -> Bool
isParallel (First ts) = not (null ts)
isParallel (Last  ts) = not (null ts)
isParallel _          = False

-- | Maximal number of outcomes that run in parallel.
--
-- * The arguments of 'First' and 'Last' run in parallel.
-- * The arguments of 'Seq' and 'Choices' do __not__ run in parallel.
maxParallel :: Term v -> Int
maxParallel (Seq   ts)    = maximum $ map maxParallel ts
maxParallel (Last  ts)    = sum $ map maxParallel ts
maxParallel (First ts)    = sum $ map maxParallel ts
maxParallel (Choices wts) = maximum $ map (maxParallel . snd) wts
maxParallel _             = 1

-- | Normalize a term to \"associative normal form\".
normalizeAssoc :: Term v -> Term v
normalizeAssoc = everywhere normalize1Assoc

-- | Apply a transformation everywhere; bottom-up.
--
-- See also [Scrap your boilerplate
-- ](https://www.microsoft.com/en-us/research/wp-content/uploads/2003/01/hmap.pdf)
-- .
everywhere :: (Term v -> Term v) -> Term v -> Term v
everywhere f = every
  where
    every = f . recurse

    recurse a@(Var _) = a
    recurse a@Never = a
    recurse a@Wait0 = a
    recurse a@(Wait _) = a
    recurse a@(Uniform _ _) = a
    recurse a@(Loc  _) = a
    recurse (Seq   xs) = Seq $ map every xs
    recurse (Last  xs) = Last $ map every xs
    recurse (First xs) = First $ map every xs
    recurse (Choices wxs) = Choices $ [ (w, every x) | (w, x) <- wxs]

-- | Summarize all nodes; top-down, left-to-right.
--
-- See also [Scrap your boilerplate
-- ](https://www.microsoft.com/en-us/research/wp-content/uploads/2003/01/hmap.pdf)
-- .
everything :: (r -> r -> r) -> (Term v -> r) -> (Term v -> r)
everything combine f = recurse
  where
    recurse x@(Var _)  = f x
    recurse x@Never    = f x
    recurse x@Wait0    = f x
    recurse x@(Wait _) = f x
    recurse x@(Uniform _ _) = f x
    recurse x@(Loc  _) = f x
    recurse x@(Seq   xs) =
        foldl' combine (f x) $ map recurse xs
    recurse x@(Last  xs) =
        foldl' combine (f x) $ map recurse xs
    recurse x@(First xs) =
        foldl' combine (f x) $ map recurse xs
    recurse x@(Choices wxs) =
        foldl' combine (f x) $ [ recurse y | (_, y) <- wxs]

-- | Normalize a term to \"associative normal form\"
-- under the assumptions
-- that the arguments to the outermost constructor are already normalized.
normalize1Assoc :: Term v -> Term v
normalize1Assoc = id
    . normalizeEmpty
    . normalize1Assoc'

-- | Ensure that the lists in 'Seq', 'Last', 'First'
-- do not contain a term with the same constructor.
normalizeEmpty :: Term v -> Term v
normalizeEmpty (Seq   []) = Wait0
normalizeEmpty (Last  []) = Wait0
normalizeEmpty (First []) = Wait0
normalizeEmpty (Choices []) = Wait0
normalizeEmpty x = x

-- | Ensure that the lists in 'Seq', 'Last', 'First'
-- do not contain a term with the same constructor.
normalize1Assoc' :: Term v -> Term v
normalize1Assoc' (Seq xs) = Seq (concatMap f xs)
  where
    f Wait0 = []
    f (Seq ys) = ys
    f o = [o]
normalize1Assoc' (Last xs) = Last (concatMap f xs)
  where
    f Wait0 = []
    f (Last ys) = ys
    f o = [o]
normalize1Assoc' (First xs) = First (concatMap f xs)
  where
    f Wait0 = []
    f (First ys) = ys
    f o = [o]
normalize1Assoc' o = o