An implementation of Skiba et al. (2012)'s W′ model in Haskell.
Last active
February 2, 2018 03:03
-
-
Save jmackie/15ef439f219aa0b8f7edf2a2f37527dd to your computer and use it in GitHub Desktop.
W' balance modelling with Haskell
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
stack wprime.hs | gnuplot -p -e 'plot "/dev/stdin" using 0:1 with lines' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env stack | |
{- stack script --resolver lts-9.13 -} | |
{-# LANGUAGE GeneralizedNewtypeDeriving #-} | |
{-# LANGUAGE NamedFieldPuns #-} | |
import qualified Data.DList as DL | |
infixl 0 # | |
(#) :: a -> (a -> c) -> c | |
(#) = flip ($) | |
-- DATA ----------------------------------------------------------------------- | |
-- Aliases for SI units. | |
-- NOTE: Not deriving from Num and friends because callers | |
-- should have to explicitly use value constructors, rather | |
-- than use numeric literals via fromInteger/fromRational! | |
newtype Power = Watts { watts :: Double } deriving (Eq, Ord) | |
newtype Energy = Joules { joules :: Double } deriving (Eq, Ord) | |
newtype Time = Seconds { seconds :: Integer } deriving (Enum) | |
-- Helpful synonym. | |
type Timestamp = Time | |
type DiffTime = Time -- NOTE: we don't need Data.Time.Clock's precision | |
-- A "segment of an exercise session". | |
data Segment = Segment { segStart :: Timestamp | |
, segEnd :: Timestamp | |
, segPwr :: Power -- mean | |
} | |
-- Exercise intensity domains. We don't actually use moderate here, | |
-- it's included for completeness. | |
data Domain | |
= Moderate | |
| Heavy | |
| Severe | |
deriving (Eq, Ord) | |
-- Athlete descriptors. | |
data Athlete = Athlete { athleteCP :: Power | |
, athleteLT :: Power -- not currently used | |
, athleteW' :: Energy | |
, athleteTau :: Power -> Double -- :: DCP -> Tau | |
} | |
-- DATA HELPERS --------------------------------------------------------------- | |
-- Returns the length of a segment in seconds. | |
segLen :: Segment -> DiffTime | |
segLen Segment{ segStart, segEnd } = | |
Seconds $ seconds segEnd - seconds segStart | |
-- Returns the intensity domain of a segment for a given athlete. | |
segDomain :: Athlete -> Segment -> Domain | |
segDomain Athlete{ athleteCP, athleteLT } Segment{ segPwr } | |
| segPwr > athleteCP = Severe | |
| segPwr > athleteLT = Heavy | |
| otherwise = Moderate | |
-- Skiba et al., 2012 | |
defaultTau :: Power -> Double | |
defaultTau (Watts dcp) = 546 * exp (negate 0.01 * dcp) + 316 | |
-- CALCULATIONS --------------------------------------------------------------- | |
-- Calculate W' balance for consecutive segments of an exercise session. | |
calcBalance :: Athlete -> [Segment] -> [Energy] | |
calcBalance athlete@Athlete{ athleteW' } = DL.toList . calcFrom athleteW' | |
where | |
calcFrom :: Energy -> [Segment] -> DL.DList Energy | |
calcFrom _ [] = DL.empty | |
calcFrom bal segs@(seg1:_) = | |
let | |
(section, rest) = | |
span ((== domain seg1). domain) segs | |
bals = | |
if domain seg1 >= Severe | |
then scanl (expendW' athlete) bal section | |
else map (recoverW' athlete seg1 bal) section | |
in | |
DL.fromList bals `DL.append` calcFrom (last bals) rest | |
domain :: Segment -> Domain | |
domain = segDomain athlete -- shorthand | |
-- Discharge function. | |
-- NOTE: can't drop below zero. | |
expendW' :: Athlete -> Energy -> Segment -> Energy | |
expendW' a (Joules bal) seg = Joules $ max 0 $ bal - (pwr - cp) * dt | |
where pwr = watts . segPwr $ seg | |
cp = watts . athleteCP $ a | |
dt = fromIntegral . seconds . segLen $ seg | |
-- Recharge function. | |
recoverW' :: Athlete -> Segment -> Energy -> Segment -> Energy | |
recoverW' Athlete{ athleteCP, athleteW', athleteTau } | |
Segment{ segStart = u } | |
(Joules bal) | |
Segment{ segEnd = t, segPwr } | |
| bal < w' = Joules $ w' - wexp * exp (negate tu / athleteTau dcp) | |
| otherwise = Joules w' | |
where | |
pwr = segPwr # watts | |
cp = athleteCP # watts | |
tu = fromIntegral $ seconds t - seconds u | |
dcp = Watts $ cp - pwr | |
w' = athleteW' # joules | |
wexp = w' - bal -- W' expended | |
-- TESTING -------------------------------------------------------------------- | |
exampleAthlete :: Athlete | |
exampleAthlete = | |
Athlete { athleteCP = 300 # Watts | |
, athleteLT = 200 # Watts | |
, athleteW' = 21e3 # Joules | |
, athleteTau = defaultTau | |
} | |
-- Second time addition. | |
tadd :: Time -> Time -> Time | |
tadd (Seconds x) (Seconds y) = Seconds (x + y) | |
exampleSegments :: [Segment] | |
exampleSegments = | |
segs (Watts 350) (Seconds 0) (Seconds 800) (Seconds 1) | |
++ segs (Watts 100) (Seconds 801) (Seconds 1200) (Seconds 1) | |
where segs pwr start stop step = | |
[ Segment{segStart = i, segEnd = i `tadd` step, segPwr = pwr} | |
| i <- [start, start `tadd` step..start `tadd` stop] | |
] | |
instance Show Energy where | |
show (Joules e) = (++" kJ") . show $ e / 1000 | |
main :: IO () | |
main = do | |
let bals = calcBalance exampleAthlete exampleSegments | |
mapM_ print bals | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment