epi-sim-0.7.0: src/Epidemic/Types/Time.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE RecordWildCards #-}
module Epidemic.Types.Time
( AbsoluteTime(..)
, TimeDelta(..)
, TimeInterval(..)
, Timed(..)
, TimeStamp(..)
, allTimes
, allValues
, asConsecutiveIntervals1
, asTimed
, cadlagValue
, diracDeltaValue
, hasTime
, inInterval
, isAscending
, maybeNextTimed
, nextTime
, timeAfterDelta
, timeDelta
, timeInterval1
, timeInterval2
) where
import qualified Data.Aeson as Json
import qualified Data.List as List
import qualified Data.Maybe as Maybe
import GHC.Generics
-- | Absolute time.
newtype AbsoluteTime =
AbsoluteTime Double
deriving (Generic, Eq, Show, Ord)
instance Json.FromJSON AbsoluteTime
instance Json.ToJSON AbsoluteTime
-- | A type that has an absolute time associated with it and can be treated as
-- having a temporal ordering.
--
-- > a = AbsoluteTime 1
-- > b = AbsoluteTime 2
-- > a `isBefore` b
--
class TimeStamp a where
absTime :: a -> AbsoluteTime
isAfter :: a -> a -> Bool
isAfter x y = absTime x > absTime y
isBefore :: a -> a -> Bool
isBefore x y = absTime x < absTime y
instance TimeStamp AbsoluteTime where
absTime = id
-- | Duration of time between two absolute times.
newtype TimeDelta =
TimeDelta Double
deriving (Generic, Eq, Show, Ord)
instance Json.FromJSON TimeDelta
instance Json.ToJSON TimeDelta
-- | An interval of time
data TimeInterval =
TimeInterval
{ timeIntEndPoints :: (AbsoluteTime, AbsoluteTime)
, timeIntDuration :: TimeDelta
}
deriving (Generic, Eq, Show)
instance Json.FromJSON TimeInterval
instance Json.ToJSON TimeInterval
-- | The duration of time between two absolute times
--
-- >>> timeDelta (AbsoluteTime 1) (AbsoluteTime 2.5)
-- TimeDelta 1.5
--
timeDelta ::
AbsoluteTime -- ^ start
-> AbsoluteTime -- ^ finish
-> TimeDelta
timeDelta (AbsoluteTime t0) (AbsoluteTime t1) = TimeDelta (t1 - t0)
-- | The time after a given delay
--
-- >>> timeAfterDelta (AbsoluteTime 1) (TimeDelta 2.5)
-- AbsoluteTime 3.5
--
timeAfterDelta :: AbsoluteTime -> TimeDelta -> AbsoluteTime
timeAfterDelta (AbsoluteTime t0) (TimeDelta d) = AbsoluteTime (t0 + d)
-- | Construct a 'TimeInterval' from the end points.
timeInterval1 :: AbsoluteTime -> AbsoluteTime -> TimeInterval
timeInterval1 start end = TimeInterval (start, end) (timeDelta start end)
-- | Construct a 'TimeInterval' from the start time and the duration.
timeInterval2 :: AbsoluteTime -> TimeDelta -> TimeInterval
timeInterval2 start duration =
TimeInterval (start, timeAfterDelta start duration) duration
-- | Check if an 'AbsoluteTime' sits within a 'TimeInterval'.
inInterval :: TimeStamp a => TimeInterval -> a -> Bool
inInterval TimeInterval {..} x =
let (start, end) = timeIntEndPoints
absT = absTime x
in start <= absT && absT <= end
-- | Construct a list of consecutive intervals divided by the given absolute
-- times.
asConsecutiveIntervals1 :: [AbsoluteTime] -> [TimeInterval]
asConsecutiveIntervals1 absTimes =
zipWith timeInterval1 (init absTimes) (tail absTimes)
-- | Type containing values at times. The times are increasing as required by
-- @asTimed@.
newtype Timed a =
Timed [(AbsoluteTime, a)]
deriving (Generic, Eq, Show)
instance Json.FromJSON a => Json.FromJSON (Timed a)
instance Json.ToJSON a => Json.ToJSON (Timed a)
instance Semigroup (Timed a) where
(Timed x) <> (Timed y) = Timed $ List.sortOn fst (x ++ y)
-- | Construct a timed list if possible.
asTimed ::
Num a
=> [(AbsoluteTime, a)] -- ^ list of ascending times and values
-> Maybe (Timed a)
asTimed tas =
if isAscending $ map fst tas
then Just (Timed $ tas ++ [(AbsoluteTime (1 / 0), -1)])
else Nothing
-- | Predicate to check if a list of orderable objects is in ascending order.
isAscending :: Ord a => [a] -> Bool
isAscending xs =
case xs of
[] -> True
[_] -> True
(x:y:xs') -> x <= y && isAscending (y : xs')
-- | Evaluate the timed object treating it as a cadlag function
cadlagValue :: Timed a -> AbsoluteTime -> Maybe a
cadlagValue (Timed txs) = cadlagValue' txs
cadlagValue' :: [(AbsoluteTime, a)] -> AbsoluteTime -> Maybe a
cadlagValue' [] _ = Nothing
cadlagValue' ((t, x):txs) q =
if q < t
then Nothing
else let nextCLV = cadlagValue' txs q
in if Maybe.isNothing nextCLV
then Just x
else nextCLV
-- | Evaluate the timed object treating it as a direct delta function
diracDeltaValue :: Timed a -> AbsoluteTime -> Maybe a
diracDeltaValue (Timed txs) = diracDeltaValue' txs
diracDeltaValue' :: [(AbsoluteTime, a)] -> AbsoluteTime -> Maybe a
diracDeltaValue' txs q =
case txs of
((t, x):txs') ->
if t == q
then Just x
else diracDeltaValue' txs' q
[] -> Nothing
-- | Check if there exists a pair with a particular time index.
hasTime :: Timed a -> AbsoluteTime -> Bool
hasTime tx absT = elem absT $ allTimes tx
-- | Return the value of the next time if possible or an exact match if it
-- exists.
nextTime :: Timed a -> AbsoluteTime -> Maybe AbsoluteTime
nextTime (Timed txs) = nextTime' txs
nextTime' :: [(AbsoluteTime, a)] -> AbsoluteTime -> Maybe AbsoluteTime
nextTime' txs q =
case txs of
((t, _):txs') ->
if q < t
then Just t
else nextTime' txs' q
[] -> Nothing
-- | Return a list of the (finite) absolute times that the step function changes
-- value.
--
-- >>> let demoMaybeTimed = asTimed [(AbsoluteTime 1,2),(AbsoluteTime 1.5,1)]
-- >>> liftM allTimes demoMaybeTimed
-- Just [AbsoluteTime 1.0,AbsoluteTime 1.5]
--
allTimes :: Timed a -> [AbsoluteTime]
allTimes (Timed txs) = [t | (t, _) <- txs, not $ isInfiniteAbsoluteTime t]
-- | The values that the timed variable takes. NOTE that it is safe to use
-- 'fromJust' here because 'allTimes' only returns times for which there is a
-- cadlag value anyway.
--
-- >>> (Just tx) = asTimed [(AbsoluteTime 1,2),(AbsoluteTime 1.5,1)]
-- >>> allValues tx
-- [2,1]
--
allValues :: Timed a -> [a]
allValues timed = Maybe.fromJust . cadlagValue timed <$> allTimes timed
-- | Predicate for an infinite absolute time
isInfiniteAbsoluteTime :: AbsoluteTime -> Bool
isInfiniteAbsoluteTime (AbsoluteTime t) = isInfinite t
-- | Look at both of the timed objects and, if possible, return the time that
-- the first one changes along with the value it changes to.
--
-- >>> (Just tA) = asTimed [(AbsoluteTime 1, (1.1 :: Double)), (AbsoluteTime 3, 2.3)]
-- >>> (Just tB) = asTimed [(AbsoluteTime 2, (1 :: Int))]
-- >>> maybeNextTimed tA tB (AbsoluteTime 0.5)
-- Just (AbsoluteTime 1.0,Left 1.1)
-- >>> maybeNextTimed tA tB (AbsoluteTime 1.5)
-- Just (AbsoluteTime 2.0,Right 1)
-- >>> maybeNextTimed tA tB (AbsoluteTime 3.5)
-- Nothing
--
maybeNextTimed :: Timed a
-> Timed b
-> AbsoluteTime
-> Maybe (AbsoluteTime, Either a b)
maybeNextTimed timedA timedB absT =
let f = flip nextTime absT
g1 timed at = do -- two functions are needed for the different types.
v <- diracDeltaValue timed at
if isInfiniteAbsoluteTime at
then Nothing
else Just (at, Left v)
g2 timed at = do
v <- diracDeltaValue timed at
if isInfiniteAbsoluteTime at
then Nothing
else Just (at, Right v)
in case (f timedA, f timedB) of
(Just tA, Just tB) ->
if tA < tB
then g1 timedA tA
else g2 timedB tB
(Just tA, Nothing) -> g1 timedA tA
(Nothing, Just tB) -> g2 timedB tB
(Nothing, Nothing) -> Nothing