packages feed

currycarbon-0.6.0.0: src/Currycarbon/CLI/RunCalibrate.hs

{-# LANGUAGE LambdaCase #-}

module Currycarbon.CLI.RunCalibrate
    (CalibrateOptions (..), CalibrateInput (..), runCalibrate) where

import           Currycarbon.CalCurves
import           Currycarbon.Calibration.Calibration
import           Currycarbon.Calibration.Utils
import           Currycarbon.Parsers
import           Currycarbon.SumCalibration
import           Currycarbon.TSV
import           Currycarbon.Types
import           Currycarbon.Utils

import           Control.Exception                   (throwIO)
import           Control.Monad                       (unless, when)
import           Data.Maybe                          (fromJust, isJust)
import           System.Exit                         (exitFailure, exitSuccess)
import           System.IO                           (hPutStrLn, stderr)
import qualified System.Random                       as R

-- | A data type to represent the options to the CLI module function runCalibrate
data CalibrateOptions = CalibrateOptions {
        _calibrateInput                   :: CalibrateInput
      , _calibrateCalCurveFile            :: CalCurveSelection -- ^ Either a preloaded calibration curve or a path to a .14c file
      , _calibrateCalibrationMethod       :: CalibrationMethod -- ^ Calibration algorithm that should be used
      , _calibrateAllowOutside            :: Bool -- ^ Allow calibration to run outside of the range of the calibration curve
      , _calibrateDontInterpolateCalCurve :: Bool -- ^ Don't interpolate the calibration curve
      , _calibrateDontTrimCalCurve        :: Bool -- ^ Don't trim the calibration curve before the calibration
      , _calibrateDontTrimOutCalPDF       :: Bool -- ^ Don't trim the output CalPDF
      , _calibrateQuiet                   :: Bool -- ^ Suppress the printing of calibration results to the command line
      , _calibrateStdOutEncoding          :: String -- ^ Encoding of the stdout stream (show TextEncoding)
      , _calibrateBasicFile               :: Maybe FilePath -- ^ Path to an output file (see CLI documentation)
      , _calibrateDensityFile             :: Maybe FilePath -- ^ Path to an output file
      , _calibrateHDRFile                 :: Maybe FilePath -- ^ Path to an output file
      , _calibrateAgeSampling             :: Maybe (Maybe Word, Word, FilePath) -- ^ Settings for the age sampling
      , _calibrateCalCurveSegmentFile     :: Maybe FilePath -- ^ Path to an output file
      , _calibrateCalCurveMatrixFile      :: Maybe FilePath -- ^ Path to an output file
    }

-- | A data type with input options for the calibration
data CalibrateInput =
      CalibrateExprs [NamedCalExpr] -- ^ String listing the uncalibrated dates in CalExpr format that should be calibrated
    | CalibrateExprFile FilePath -- ^ A file with uncalibrated dates in CalExpr to be calibrated
    | CalibrateTSVFile FilePath CombinationStrategy -- ^ A file with uncalibrated dates in .tsv format to be calibrated and
                                                    -- a strategy to combine multiple C14 ages

-- | Interface function to trigger calibration from the command line
runCalibrate :: CalibrateOptions -> IO ()
runCalibrate (
        CalibrateOptions
            input
            calCurveSelection method allowOutside noInterpolate noTrimCalCurve noTrimOutCalPDF
            quiet encoding
            basicFile densityFile hdrFile
            ageSampling
            calCurveSegmentFile calCurveMatrixFile
        ) = do
    let ascii = encoding /= "UTF-8"
    -- compile dates
    (errOrExprs,_) <- case input of
        CalibrateExprs exprs -> do
            return (map Right exprs, Nothing)
        CalibrateExprFile path -> do
            exprs <- readNamedCalExprsFromFile path
            return (map Right exprs, Nothing)
        CalibrateTSVFile path combStrat -> do
            tsv <- readTSV path
            let exprs = tsv2NamedCalExprs combStrat tsv
            return (exprs, Just tsv)
    let exprsRenamed = replaceEmptyNames errOrExprs
    if null exprsRenamed
    then hPutStrLn stderr "Nothing to calibrate. See currycarbon -h for help"
    else do
        -- prep data
        hPutStrLn stderr $ "Method: " ++ show method
        hPutStrLn stderr $ "Curve: " ++ show calCurveSelection
        calCurve <- getCalCurve calCurveSelection
        let calConf = defaultCalConf {
              _calConfAllowOutside = allowOutside
            , _calConfInterpolateCalCurve = not noInterpolate
            , _calConfTrimCalCurveBeforeCalibration = not noTrimCalCurve
            -- previously set to: not noTrimOutCalPDF
            -- but for the command line app it's better to always do this later (see below!)
            , _calConfTrimCalPDFAfterCalibration = False
            }
        -- handle the special debug cases
        when (isJust calCurveSegmentFile || isJust calCurveMatrixFile) $ do
            case exprsRenamed of
                [Left e] -> throwIO e
                [Right (NamedCalExpr _ (UnCalDate uncal))] -> do
                    let calCurveSegment = prepareCalCurveSegment (not noInterpolate) $
                            getRelevantCalCurveSegment uncal calCurve
                    when (isJust calCurveSegmentFile) $
                        writeCalCurve (fromJust calCurveSegmentFile) calCurveSegment
                    when (isJust calCurveMatrixFile) $
                        writeCalCurveMatrix (fromJust calCurveMatrixFile) $
                        makeCalCurveMatrix (uncalToPDF uncal) calCurveSegment
                _ -> do
                    throwIO $ CurrycarbonCLIException
                        "--calCurveSegFile and --calCurveMatFile only work with \
                        \a single uncalibrated radiocarbon date."
        -- run calibration
        hPutStrLn stderr "Calibrating..."
        let errorOrCalPDFs = fmap (either Left (evalNamedCalExpr method calConf calCurve)) exprsRenamed
        -- trim output
            calRes = if not noTrimOutCalPDF
                     then map (mapEither id trimLowDensityEdgesCalPDF) errorOrCalPDFs
                     else errorOrCalPDFs
        -- prepare random number generator for age sampling
        maybeRNG <- case ageSampling of
            Nothing -> pure Nothing
            Just (maybeSeed, _, _) -> case maybeSeed of
                Nothing   -> Just <$> R.initStdGen
                Just seed -> return $ Just $ R.mkStdGen (fromIntegral seed)
        -- prepare and write the output per expression
        handleExprs ascii True maybeRNG calCurve $ zip3 [1..] exprsRenamed calRes
        -- final conclusion
        let calibrationErrors =
                [ (i, e)
                | (i, Left e) <- zip [1..] calRes
                ]
            refinementErrors =
                [ (i, e)
                | (i, Right calPDF) <- zip [1..] calRes
                , Left e <- [validateCalPDF calPDF]
                ]
            errors = calibrationErrors ++ refinementErrors
        if null errors
        then do
            hPutStrLn stderr "---"
            hPutStrLn stderr "No breaking issues."
            exitSuccess
        else do
            hPutStrLn stderr "---"
            hPutStrLn stderr "Failed expressions:"
            mapM_ (uncurry printE) errors
            exitFailure


        where
        -- loop over first and subsequent expressions
        handleExprs ::
               Bool -- encoding
            -> Bool -- is this expression the first in the list of expressions?
            -> Maybe R.StdGen -- rng for the age sampling seeds
            -> CalCurveBP
            -> [(Integer, Either CurrycarbonException NamedCalExpr, Either CurrycarbonException CalPDF)]
            -> IO ()
        handleExprs _ _ _ _ [] = hPutStrLn stderr "Done."
        -- first expression
        handleExprs _ascii True maybeRNG calCurve (firstDate:otherDates) =
            case firstDate of
                (i, Right ex, Right cPDF) -> do
                    unless quiet $ hPutStrLn stderr $ "--- " ++ show i ++ " ---"
                    let (sampleSeed, newRNG) = drawSeed maybeRNG
                    flexOut _ascii calCurve i ex cPDF sampleSeed writeCalPDF writeCalC14CalRangeSummary writeCalC14HDR writeRandomAgeSample
                    handleExprs _ascii False newRNG calCurve otherDates
                (i, _, Left e) -> do
                    printE i e
                    handleExprs _ascii True maybeRNG calCurve otherDates
                _ -> error "can not happen"
        -- subsequent expression
        handleExprs _ascii False maybeRNG calCurve (nextDate:otherDates) =
            case nextDate of
                (i, Right ex, Right cPDF) -> do
                    unless quiet $ hPutStrLn stderr $ "--- " ++ show i ++ " ---"
                    let (sampleSeed, newRNG) = drawSeed maybeRNG
                    flexOut _ascii calCurve i ex cPDF sampleSeed appendCalPDF appendCalC14CalRangeSummary appendCalC14HDR appendRandomAgeSample
                    handleExprs _ascii False newRNG calCurve otherDates
                (i, _, Left e) -> do
                    printE i e
                    handleExprs _ascii False maybeRNG calCurve otherDates
                _ -> error "can not happen"

        printE :: Integer -> CurrycarbonException -> IO ()
        printE i e = hPutStrLn stderr $ show i ++ ": " ++ renderCurrycarbonException e

        drawSeed :: Maybe R.StdGen -> (Maybe Int, Maybe R.StdGen)
        drawSeed maybeRNG = (\x -> (fromIntegral . fst <$> x, snd <$> x)) (R.genWord32 <$> maybeRNG)

        -- flexible expression handler
        flexOut ::
               Bool
            -> CalCurveBP
            -> Integer
            -> NamedCalExpr
            -> CalPDF
            -> Maybe Int
            -> (FilePath -> CalPDF -> IO ())
            -> (FilePath -> CalC14 -> IO ())
            -> (FilePath -> CalC14 -> IO ())
            -> (FilePath -> RandomAgeSample -> IO ())
            -> IO ()
        flexOut _ascii calCurve i namedCalExpr calPDF maybeSeed calPDFToFile calC14CalRangeSummaryToFile calC14HDRToFile randomAgeSampleToFile = do
            case refineCalDate calPDF of
                Left e -> do
                    unless quiet $ do
                        putStrLn ("CalEXPR: " ++ renderNamedCalExpr namedCalExpr)
                        printE i e
                    when (isJust basicFile) $ unless quiet $
                        hPutStrLn stderr "<!> Error: Can not create --basicFile"
                    when (isJust hdrFile) $ unless quiet $
                        hPutStrLn stderr "<!> Error: Can not create --hdrFile"
                Right calC14 -> do
                    unless quiet $ do
                        putStrLn (renderCalDatePretty _ascii calCurve (not noInterpolate) (namedCalExpr, calPDF, calC14))
                    when (isJust basicFile) $
                        calC14CalRangeSummaryToFile (fromJust basicFile) calC14
                    when (isJust hdrFile) $
                        calC14HDRToFile (fromJust hdrFile) calC14
            when (isJust ageSampling && isJust maybeSeed) $ do
                let (_, nrOfSamples, path) = fromJust ageSampling
                    rng = R.mkStdGen (fromJust maybeSeed)
                    conf = AgeSamplingConf rng nrOfSamples
                case sampleAgesFromCalPDF conf calPDF of
                    Left e -> do
                        unless quiet $ do
                            printE i e
                            hPutStrLn stderr "<!> Error: Can not create --samplesFile"
                    Right res -> randomAgeSampleToFile path res
            when (isJust densityFile) $
                calPDFToFile (fromJust densityFile) calPDF

-- | Helper function to replace empty input names with a sequence of numbers,
-- to get each input date an unique identifier
replaceEmptyNames :: [Either CurrycarbonException NamedCalExpr] -> [Either CurrycarbonException NamedCalExpr]
replaceEmptyNames = zipWith modify [1..]
    where
        modify :: Integer -> Either CurrycarbonException NamedCalExpr -> Either CurrycarbonException NamedCalExpr
        modify _ (Left e)  = Left e
        modify i (Right x) = Right $ modifyNamedExpr (show i) x
        modifyNamedExpr :: String -> NamedCalExpr -> NamedCalExpr
        modifyNamedExpr i nexpr =
            if _exprID nexpr == ""
            then nexpr { _exprID = i, _expr = replaceName i (_expr nexpr) }
            else nexpr {              _expr = replaceName i (_expr nexpr) }
        replaceName :: String -> CalExpr -> CalExpr
        replaceName i (UnCalDate (UncalC14 name x y)) =
            if name == ""
            then UnCalDate $ UncalC14 i x y
            else UnCalDate $ UncalC14 name x y
        replaceName i (WindowBP (TimeWindowBP name start stop)) =
            if name == ""
            then WindowBP $ TimeWindowBP i start stop
            else WindowBP $ TimeWindowBP name start stop
        replaceName i (WindowBCAD (TimeWindowBCAD name start stop)) =
            if name == ""
            then WindowBCAD $ TimeWindowBCAD i start stop
            else WindowBCAD $ TimeWindowBCAD name start stop
        replaceName i (CalDate (CalPDF name x y)) =
            if name == ""
            then CalDate $ CalPDF i x y
            else CalDate $ CalPDF name x y
        replaceName i (SumCal a b)     = SumCal (replaceName (i ++ "s") a) (replaceName (i ++ "S") b)
        replaceName i (ProductCal a b) = ProductCal (replaceName (i ++ "p") a) (replaceName (i ++ "P") b)