currycarbon-0.4.0.0: src/Currycarbon/CLI/RunCalibrate.hs
module Currycarbon.CLI.RunCalibrate
(CalibrateOptions (..), runCalibrate) where
import Currycarbon.CalCurves
import Currycarbon.Calibration.Calibration
import Currycarbon.Calibration.Utils
import Currycarbon.Parsers
import Currycarbon.SumCalibration
import Currycarbon.Types
import Currycarbon.Utils
import Control.Exception (throwIO)
import Control.Monad (unless, when)
import Data.Maybe (fromJust, isJust)
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 {
_calibrateExprs :: [NamedCalExpr] -- ^ String listing the uncalibrated dates that should be calibrated
, _calibrateExprFiles :: [FilePath] -- ^ List of files with uncalibrated dates to be calibrated
, _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
}
-- | Interface function to trigger calibration from the command line
runCalibrate :: CalibrateOptions -> IO ()
runCalibrate (
CalibrateOptions
exprs exprFiles
calCurveSelection method allowOutside noInterpolate noTrimCalCurve noTrimOutCalPDF
quiet encoding
basicFile densityFile hdrFile
ageSampling
calCurveSegmentFile calCurveMatrixFile
) = do
let ascii = encoding /= "UTF-8"
-- compile dates
exprsFromFile <- mapM readNamedCalExprsFromFile exprFiles
let exprsCombined = exprs ++ concat exprsFromFile
exprsRenamed = replaceEmptyNames exprsCombined
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
[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 = map (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 calCurve maybeRNG $ zip exprsRenamed calRes
where
-- loop over first and subsequent expressions
handleExprs ::
Bool -- encoding
-> Bool -- is this expression the first in the list of expressions?
-> CalCurveBP
-> Maybe R.StdGen -- rng for the age sampling seeds
-> [(NamedCalExpr, Either CurrycarbonException CalPDF)]
-> IO ()
handleExprs _ _ _ _ [] = hPutStrLn stderr "Done."
-- first expression
handleExprs _ascii True calCurve maybeRNG (firstDate:otherDates) =
case firstDate of
(_, Left e) -> do
printE e
handleExprs _ascii True calCurve maybeRNG otherDates
(namedCalExpr, Right cPDF) -> do
let (sampleSeed, newRNG) = drawSeed maybeRNG
flexOut _ascii namedCalExpr cPDF sampleSeed writeCalPDF writeCalC14CalRangeSummary writeCalC14HDR writeRandomAgeSample
handleExprs _ascii False calCurve newRNG otherDates
-- subsequent expression
handleExprs _ascii False calCurve maybeRNG (nextDate:otherDates) =
case nextDate of
(_, Left e) -> do
printE e
handleExprs _ascii False calCurve maybeRNG otherDates
(namedCalExpr, Right cPDF) -> do
let (sampleSeed, newRNG) = drawSeed maybeRNG
flexOut _ascii namedCalExpr cPDF sampleSeed appendCalPDF appendCalC14CalRangeSummary appendCalC14HDR appendRandomAgeSample
handleExprs _ascii False calCurve newRNG otherDates
printE :: CurrycarbonException -> IO ()
printE e = hPutStrLn stderr $ 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
-> NamedCalExpr
-> CalPDF
-> Maybe Int
-> (FilePath -> CalPDF -> IO ())
-> (FilePath -> CalC14 -> IO ())
-> (FilePath -> CalC14 -> IO ())
-> (FilePath -> RandomAgeSample -> IO ())
-> IO ()
flexOut _ascii namedCalExpr calPDF maybeSeed calPDFToFile calC14CalRangeSummaryToFile calC14HDRToFile randomAgeSampleToFile = do
case refineCalDate calPDF of
Left e -> do
unless quiet $ do
putStrLn ("CalEXPR: " ++ renderNamedCalExpr namedCalExpr)
printE 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 (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 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 :: [NamedCalExpr] -> [NamedCalExpr]
replaceEmptyNames = zipWith (modifyNamedExpr . show) ([1..] :: [Integer])
where
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)