currycarbon-0.6.0.0: src/Currycarbon/CalCurves.hs
{-# LANGUAGE TemplateHaskell #-}
module Currycarbon.CalCurves where
import Currycarbon.ParserHelpers
import Currycarbon.Types
import Currycarbon.Utils
import Control.Exception (throwIO)
import qualified Data.FileEmbed as FE
import qualified Data.Vector.Unboxed as VU
import qualified Text.Parsec as P
import qualified Text.Parsec.String as P
data CalCurveSelection =
IntCal20 | SHCal20 | Marine20
| CalCurveFromFile FilePath
instance Show CalCurveSelection where
show IntCal20 = "IntCal20"
show SHCal20 = "SHCal20"
show Marine20 = "Marine20"
show (CalCurveFromFile path) = path
readCalCurveSelection :: String -> Either String CalCurveSelection
readCalCurveSelection s =
case P.runParser parseCalCurveSelection () s s of
Left err -> Left $ showParsecErrOneLine err
Right x -> Right x
parseCalCurveSelection :: P.Parser CalCurveSelection
parseCalCurveSelection = do
x <- P.many P.anyChar
case x of
"IntCal20" -> pure IntCal20
"SHCal20" -> pure SHCal20
"Marine20" -> pure Marine20
p -> return $ CalCurveFromFile p
getCalCurve :: CalCurveSelection -> IO CalCurveBP
getCalCurve IntCal20 = pure intcal20
getCalCurve SHCal20 = pure shcal20
getCalCurve Marine20 = pure marine20
getCalCurve (CalCurveFromFile path) = readCalCurveFromFile path
-- | Read a calibration curve file. The file must adhere to the .14c file format.
readCalCurveFromFile :: FilePath -> IO CalCurveBP
readCalCurveFromFile calCurveFile = do
calCurveString <- readFile calCurveFile
case readCalCurve calCurveString of
Left err -> throwIO $ CurrycarbonCLIParsingException err
Right x -> return x
readCalCurve :: String -> Either String CalCurveBP
readCalCurve calCurveString = do
case P.runParser parseCalCurve () "calCurve" calCurveString of
Left err -> Left $ showParsecErr err
Right x -> Right $ CalCurveBP
(VU.fromList $ map (\(a,_,_) -> a) x)
(VU.fromList $ map (\(_,b,_) -> b) x)
(VU.fromList $ map (\(_,_,c) -> c) x)
readCalCurveUnsafe :: String -> CalCurveBP
readCalCurveUnsafe calCurveString = do
case P.runParser parseCalCurve () "calCurve" calCurveString of
Left err -> error $ "Error when reading calCurve " ++ show err
Right x -> CalCurveBP
(VU.fromList $ map (\(a,_,_) -> a) x)
(VU.fromList $ map (\(_,b,_) -> b) x)
(VU.fromList $ map (\(_,_,c) -> c) x)
parseCalCurve :: P.Parser [(YearBP, YearBP, YearRange)]
parseCalCurve = do
P.skipMany comments
P.sepEndBy parseCalCurveLine (P.manyTill P.anyToken (P.try P.newline))
parseCalCurveLine :: P.Parser (YearBP, YearBP, YearRange)
parseCalCurveLine = do
calBP <- parseWord
_ <- P.oneOf ","
bp <- parseWord
_ <- P.oneOf ","
sigma <- parseWord
return (calBP, bp, sigma)
comments :: P.Parser String
comments = do
_ <- P.string "#"
_ <- P.manyTill P.anyChar P.newline
return ""
-- | The intcal20 calibration curve
-- (Reimer et al. 2020, doi: [10.1017/RDC.2020.41](https://doi.org/10.1017/RDC.2020.41))
intcal20 :: CalCurveBP
intcal20 = readCalCurveUnsafe intcal20String
intcal20String :: String
intcal20String = $(FE.makeRelativeToProject "data/intcal20.14c" >>= FE.embedStringFile)
-- | The shcal20 calibration curve
-- (Hogg et al. 2020, doi: [10.1017/RDC.2020.59](https://doi.org/10.1017/RDC.2020.59))
shcal20 :: CalCurveBP
shcal20 = readCalCurveUnsafe shcal20String
shcal20String :: String
shcal20String = $(FE.makeRelativeToProject "data/shcal20.14c" >>= FE.embedStringFile)
-- | The shcal20 calibration curve
-- (Heaton et al. 2020, doi: [10.1017/RDC.2020.68](https://doi.org/10.1017/RDC.2020.68))
marine20 :: CalCurveBP
marine20 = readCalCurveUnsafe marine20String
marine20String :: String
marine20String = $(FE.makeRelativeToProject "data/marine20.14c" >>= FE.embedStringFile)
-- | Calibration curves usually do not have yearly resolution.
-- This function completes the curves with a most basic linear interpolation.
interpolateCalCurve :: CalCurveBP -> CalCurveBP
interpolateCalCurve (CalCurveBP cals uncals sigmas) =
let obs = VU.zip3 cals uncals sigmas
timeWindows = getTimeWindows obs
obsFilled = VU.concatMap fillTimeWindows timeWindows
in uncurry3 CalCurveBP $ VU.unzip3 obsFilled
where
getTimeWindows :: VU.Vector (YearBP,YearBP,YearRange) -> VU.Vector ((YearBP,YearBP,YearRange),(YearBP,YearBP,YearRange))
getTimeWindows xs = VU.zipWith (,) (VU.init xs) (VU.tail xs)
fillTimeWindows :: ((YearBP,YearBP,YearRange),(YearBP,YearBP,YearRange)) -> VU.Vector (YearBP,YearBP,YearRange)
fillTimeWindows ((calbp1,bp1,sigma1),(calbp2,bp2,sigma2)) =
if calbp1 == calbp2 || calbp1+1 == calbp2 || calbp1-1 == calbp2
then VU.singleton (calbp1,bp1,sigma1)
else
let newCals = VU.fromList [calbp1,calbp1-1..calbp2+1] -- range definition like this to trigger counting down
newBPs = VU.map (snd . getInBetweenPointsInt (calbp1,bp1) (calbp2,bp2)) newCals
newSigmas = VU.map (snd . getInBetweenPointsInt (calbp1,sigma1) (calbp2,sigma2)) newCals
in VU.zip3 newCals newBPs newSigmas
getInBetweenPointsInt :: (Word, Word) -> (Word, Word) -> Word -> (Word, Word)
getInBetweenPointsInt (x1,y1) (x2,y2) xPred =
let (_,yPred) = getInBetweenPoints (fromIntegral x1,fromIntegral y1) (fromIntegral x2,fromIntegral y2) $ fromIntegral xPred
in (xPred, round yPred)
getInBetweenPoints :: (Double, Double) -> (Double, Double) -> Double -> (Double, Double)
getInBetweenPoints (x1,y1) (x2,y2) xPred =
let yDiff = y2 - y1
xDiff = abs $ x1 - x2
yDiffPerxDiff = yDiff/xDiff
xPredRel = x1 - xPred
in (xPred, y1 + xPredRel * yDiffPerxDiff)
uncurry3 :: (a -> b -> c -> d) -> ((a, b, c) -> d)
uncurry3 f ~(a,b,c) = f a b c