packages feed

currycarbon-0.4.0.0: src-executables/Main-currycarbon.hs

{-# LANGUAGE OverloadedStrings #-}

import           Currycarbon.CalCurves
import           Currycarbon.CLI.RunCalibrate (CalibrateOptions (..),
                                               runCalibrate)
import           Currycarbon.Parsers
import           Currycarbon.Types
import           Currycarbon.Utils
import           Paths_currycarbon            (version)

import           Control.Exception            (catch)
import           Data.Version                 (showVersion)
import qualified Options.Applicative          as OP
import qualified Options.Applicative.Help     as OH
import           System.Exit                  (exitFailure)
import           System.IO                    (hGetEncoding, hPutStrLn, stderr,
                                               stdout)
-- * CLI interface configuration
--
-- $cliInterface
--
-- This module contains the necessary code to configure the currycarbon CLI interface

-- data types
data Options = CmdCalibrate CalibrateOptions

-- CLI interface configuration
main :: IO ()
main = do
    -- check stdout encoding for the CLI plot
    stdOutEncoding <- hGetEncoding stdout
    let encoding = maybe "unknown" show stdOutEncoding
    hPutStrLn stderr $ "currycarbon v" ++ showVersion version ++ " (" ++ encoding ++ ")"
    -- prepare input parsing
    cmdOpts <- OP.customExecParser p optParserInfo
    catch (runCmd encoding cmdOpts) handler
    where
        p = OP.prefs OP.showHelpOnEmpty
        handler :: CurrycarbonException -> IO ()
        handler e = do
            hPutStrLn stderr $ renderCurrycarbonException e
            exitFailure

runCmd :: String -> Options -> IO ()
runCmd enc o = case o of
    CmdCalibrate opts -> runCalibrate opts {_calibrateStdOutEncoding = enc}

optParserInfo :: OP.ParserInfo Options
optParserInfo = OP.info (OP.helper <*> versionOption <*> optParser) (
    OP.briefDesc <>
    OP.progDesc "Intercept calibration of radiocarbon dates"
    )

versionOption :: OP.Parser (a -> a)
versionOption = OP.infoOption (showVersion version) (OP.long "version" <> OP.help "Show version")

optParser :: OP.Parser Options
optParser = CmdCalibrate <$> calibrateOptParser

calibrateOptParser :: OP.Parser CalibrateOptions
calibrateOptParser = CalibrateOptions <$> optParseNamedCalExprString
                                      <*> optParseNamedCalExprFromFile
                                      <*> optParseCalCurveSelection
                                      <*> optParseCalibrationMethod
                                      <*> optParseAllowOutside
                                      <*> optParseDontInterpolateCalCurve
                                      <*> optParseDontTrimCalCurve
                                      <*> optParseDontTrimOutCalPDF
                                      <*> optParseQuiet
                                      <*> pure "unknown"
                                      <*> optParseBasicFile
                                      <*> optParseDensityFile
                                      <*> optParseHDRFile
                                      <*> optParseAgeSamplingSettings
                                      <*> optParseCalCurveSegmentFile
                                      <*> optParseCalCurveMatrixFile

-- ** Input parsing functions
--
-- $inputParsing
--
-- These functions define and handle the CLI input arguments

optParseNamedCalExprString :: OP.Parser [NamedCalExpr]
optParseNamedCalExprString = concat <$> OP.many (OP.argument (OP.eitherReader readNamedCalExprs) (
    OP.metavar "CalEXPRs" <>
    OP.helpDoc ( Just (
        "---"
        <> OH.hardline
        <> s2d "A string to specify \"calibration expressions\", so small chronological \
           \models for individual events. These can include uncalibrated radiocarbon ages, \
           \uniform age ranges and operations to combine the resulting age probability \
           \distribution as sums or products."
        <> OH.hardline <>
            s2d "The expression language includes the following functions:"
        <> OH.hardline
        <> OH.hardline <> "- calExpr(id = STRING, expr = EXPR)"
        <> OH.hardline <> "- uncalC14(id = STRING, yearBP = INT, sigma = INT)"
        <> OH.hardline <> "- rangeBP(id = STRING, start = INT, stop = INT)"
        <> OH.hardline <> "- rangeBCAD(id = STRING, start = INT, stop = INT)"
        <> OH.hardline <> "- sum(a = EXPR, b = EXPR)"
        <> OH.hardline <> "- product(a = EXPR, b = EXPR)"
        <> OH.hardline
        <> OH.hardline
        <> s2d "The order of arguments is fixed, but the argument names '<arg> =' \
           \can be left out. The 'id' arguments are optional. \
           \Some functions can be shortened with syntactic sugar:"
        <> OH.hardline
        <> OH.hardline <> "- calExpr(STRING, EXPR) -> id: EXPR"
        <> OH.hardline <> "- uncalC14(STRING, INT, INT) -> STRING,INT,INT"
        <> OH.hardline <> "- sum(EXPR, EXPR) -> EXPR + EXPR"
        <> OH.hardline <> "- product(EXPR, EXPR) -> EXPR * EXPR"
        <> OH.hardline
        <> OH.hardline
        <> s2d "Parentheses '()' can be used to specify the evaluation order within \
           \an expression. Multiple expressions can be chained, separated by ';'."
        <> OH.hardline
        <> OH.hardline
        <> "Examples:"
        <> OH.hardline <> s2d "1. Calibrate a single radiocarbon date with a mean age BP \
                          \and a one sigma standard deviation:"
        <> OH.hardline <> "\"3000,30\" or \"uncalC14(yearBP = 3000, sigma = 30)\""
        <> OH.hardline <> s2d "2. Calibrate two radiocarbon dates and sum them:"
        <> OH.hardline <> "\"(3000,30) + (3100,40)\" or"
        <> OH.hardline <> "\"sum(uncalC14(3000,30), uncalC14(3100,40))\""
        <> OH.hardline <> s2d "3. Compile a complex, named expression:"
        <> OH.hardline <> "\"Ex3: ((3000,30) + (3100,40)) * rangeBP(3200,3000)\""
        <> OH.hardline
        <> "---"
    ))
    ))

s2d :: String -> OH.Doc
s2d str = OH.fillSep $ map OH.pretty $ words str

optParseNamedCalExprFromFile :: OP.Parser [FilePath]
optParseNamedCalExprFromFile = OP.many (OP.strOption (
    OP.long "inputFile" <>
    OP.short 'i' <>
    OP.metavar "FILE" <>
    OP.help "A file with a list of calibration expressions. \
            \Formatted just as CalEXPRs, but with a new line for each input expression. \
            \CalEXPRs and --inputFile can be combined and you can provide multiple \
            \instances of --inputFile. \
            \Note that syntactic sugar allows to read simple radiocarbon dates from \
            \a headless .csv file with one sample per row: \
            \<sample name>,<mean age BP>,<one sigma standard deviation>."
    ))

optParseCalCurveSelection :: OP.Parser CalCurveSelection
optParseCalCurveSelection = OP.option (OP.eitherReader readCalCurveSelection) (
    OP.long "calCurve" <>
    OP.metavar "IntCal20 | SHCal20 | Marine20 | FILE" <>
    OP.help "Either one of the included calibration curves, or a \
            \file path to an calibration curve file in '.14c' format. \
            \The calibration curve will be read and used for calibration." <>
    OP.value IntCal20 <>
    OP.showDefault
    )

optParseCalibrationMethod :: OP.Parser CalibrationMethod
optParseCalibrationMethod = OP.option (OP.eitherReader readCalibrationMethod) (
    OP.long "method" <>
    OP.metavar "DSL" <>
    OP.helpDoc ( Just (
            s2d "The calibration algorithm that should be used: \
            \'<Method>,<Distribution>,<NumberOfDegreesOfFreedom>'. "
        <> OH.hardline <>
            s2d "The default setting is equivalent to \"Bchron,StudentT,100\" \
            \which copies the algorithm implemented in the Bchron R package. \
            \For the Bchron algorithm with a normal distribution (\"Bchron,Normal\") \
            \the degrees of freedom argument is not relevant"
        <> OH.hardline <>
            s2d "Alternatively we implemented  \"MatrixMult\", which comes without further \
            \arguments."
    )) <>
    OP.value (Bchron $ StudentTDist 100)
    )

optParseAllowOutside :: OP.Parser Bool
optParseAllowOutside = OP.switch (
    OP.long "allowOutside" <>
    OP.help "Allow calibrations to run outside the range of the calibration curve."
    )

optParseDontInterpolateCalCurve :: OP.Parser Bool
optParseDontInterpolateCalCurve = OP.switch (
    OP.long "noInterpolation" <>
    OP.help "Do not interpolate the calibration curve."
    )

optParseDontTrimCalCurve :: OP.Parser Bool
optParseDontTrimCalCurve = OP.switch (
    OP.long "noTrimCalCurve" <>
    OP.help "Do not trim the calibration curve before the calibration. \
            \If a probability distribution over the entire range \
            \of the calibration curve is needed. See also --noTrimOutCalPDF."
    )

optParseDontTrimOutCalPDF :: OP.Parser Bool
optParseDontTrimOutCalPDF = OP.switch (
    OP.long "noTrimOutCalPDF" <>
    OP.help "Do not trim the output CalPDF. \
            \If an untrimmed probability distribution is needed. \
            \See also --noTrimCalCurve."
    )

optParseQuiet :: OP.Parser Bool
optParseQuiet = OP.switch (
    OP.long "quiet" <>
    OP.short 'q' <>
    OP.help "Suppress the printing of calibration results to the command line."
    )

optParseBasicFile :: OP.Parser (Maybe FilePath)
optParseBasicFile = OP.option (Just <$> OP.str) (
    OP.long "basicFile" <>
    OP.metavar "FILE" <>
    OP.help "Path to an output file to store basic, per-expression output: \
            \The minimum start and maximum end of \
            \the high probability density regions and the median age." <>
    OP.value Nothing
    )

optParseDensityFile :: OP.Parser (Maybe FilePath)
optParseDensityFile = OP.option (Just <$> OP.str) (
    OP.long "densityFile" <>
    OP.metavar "FILE" <>
    OP.help "Path to an output file to store output densities per CalEXPR and calender \
            \year." <>
    OP.value Nothing
    )

optParseHDRFile :: OP.Parser (Maybe FilePath)
optParseHDRFile = OP.option (Just <$> OP.str) (
    OP.long "hdrFile" <>
    OP.metavar "FILE" <>
    OP.help "Path to an output file to store the high probability density regions for each \
            \CalEXPR." <>
    OP.value Nothing
    )

optParseAgeSamplingSettings :: OP.Parser (Maybe (Maybe Word, Word, FilePath))
optParseAgeSamplingSettings =
    OP.optional $ (,,) <$>
            optParseAgeSamplingConfSeed
        <*> optParseAgeSamplingConfNrOfSamples
        <*> optParseAgeSamplingFile

optParseAgeSamplingConfSeed :: OP.Parser (Maybe Word)
optParseAgeSamplingConfSeed = OP.option (Just <$> OP.auto) (
       OP.long  "seed"
    <> OP.metavar "INT"
    <> OP.help  "Seed for the random number generator for age sampling. \
                \The default causes currycarbon to fall back to a random seed."
    <> OP.value Nothing
    <> OP.showDefault
    )

optParseAgeSamplingConfNrOfSamples :: OP.Parser Word
optParseAgeSamplingConfNrOfSamples = OP.option OP.auto (
       OP.short 'n'
    <> OP.long "nrSamples"
    <> OP.metavar "INT"
    <> OP.help "Number of age samples to draw per CalEXPR."
    )

optParseAgeSamplingFile :: OP.Parser FilePath
optParseAgeSamplingFile = OP.strOption (
    OP.long "samplesFile" <>
    OP.metavar "FILE" <>
    OP.help "Path to an output file to store age samples for each CalEXPR."
    )

optParseCalCurveSegmentFile :: OP.Parser (Maybe FilePath)
optParseCalCurveSegmentFile = OP.option (Just <$> OP.str) (
    OP.long "calCurveSegFile" <>
    OP.metavar "FILE" <>
    OP.help "Path to an output file to store the relevant, interpolated calibration curve \
            \segment for the first (!) input date. \
            \This option as well as --calCurveMatFile are meant for debugging." <>
    OP.value Nothing
    )

optParseCalCurveMatrixFile :: OP.Parser (Maybe FilePath)
optParseCalCurveMatrixFile = OP.option (Just <$> OP.str) (
    OP.long "calCurveMatFile" <>
    OP.metavar "FILE" <>
    OP.help "Path to an output file which stores the relevant, interpolated calibration curve \
            \segment for the first (!) input date in a wide matrix format." <>
    OP.value Nothing
    )