srtree-db-0.1.1.0: app/Ingest.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Ingest
( IngestOpts(..)
, ingestParser
, runIngest
) where
import Control.Exception (bracket, SomeException, catch, displayException)
import Control.Monad (forM_, when, unless)
import Data.IORef
import qualified Data.ByteString.Char8 as B
import qualified Data.Text as T
import qualified Data.Vector.Unboxed as VU
import Options.Applicative
import System.IO (hIsEOF, hGetLine, stdin, openFile, IOMode(..), hClose, hPutStrLn, stderr, hFlush)
import System.Exit (exitFailure)
import System.Random (randomRIO)
import Data.SRTree (Fix(..), SRTree(..), relabelParams, countParamsUniq)
import Data.SRTree.Eval (Target)
import Data.SRTree.Datasets (loadDataset)
import Text.ParseSR (SRAlgs(..), parseSR)
import Algorithm.SRTree.NonlinearOpt (minimizeNLL')
import Algorithm.SRTree.Likelihoods (Loss(..), Distribution(..))
import Algorithm.SRTree.AD (ADBackEnd(..))
import Numeric.Optimization.NLOPT (LocalAlgorithm(..))
import Algorithm.EqSat.Storage.Import (importEqs, ImportSummary(..))
import Algorithm.EqSat.Storage.SQLite ()
import Database.SQLite3 (Database, open, close)
-- | CLI options for the ingest sub-command.
data IngestOpts = IngestOpts
{ ingestDb :: String
, ingestExprs :: String
, ingestDataset :: String
, ingestFormat :: SRAlgs
, ingestVarnames :: String
, ingestData :: String
, ingestFit :: Bool
, ingestLoss :: Loss
, ingestEqsatSteps :: Int
, ingestReparam :: Bool
, ingestHasHeader :: Bool
} deriving (Show)
ingestParser :: Parser IngestOpts
ingestParser = IngestOpts
<$> strOption
( long "db"
<> metavar "FILE"
<> help "SQLite database file path" )
<*> strOption
( long "expressions"
<> value ""
<> metavar "FILE"
<> help "File with one expression per line (empty = stdin)" )
<*> strOption
( long "dataset"
<> value ""
<> metavar "NAME"
<> help "Dataset name (required for fitting)" )
<*> option auto
( long "format"
<> value OPERON
<> metavar "FORMAT"
<> help "Expression format: OPERON, TIR, HL, BINGO, GOMEA, PYSR" )
<*> strOption
( long "varnames"
<> value "x0,x1,x2,x3,x4,x5"
<> metavar "VARNAMES"
<> help "Comma-separated variable names" )
<*> strOption
( long "data"
<> value ""
<> metavar "SPEC"
<> help "Dataset CSV spec: file:start:end:target:features:yerr" )
<*> switch
( long "fit"
<> help "Fit expressions after ingest" )
<*> option auto
( long "loss"
<> value (NLL Gaussian)
<> metavar "LOSS"
<> help "Loss function (MSE, NLL Gaussian, etc.)" )
<*> option auto
( long "eqsat-steps"
<> value 0
<> metavar "N"
<> help "Run N eqsat steps after ingest" )
<*> switch
( long "reparam"
<> help "Float constants to parameters" )
<*> switch
( long "has-header"
<> help "CSV has header row (default: True)" )
-- | Run the ingest sub-command.
runIngest :: IngestOpts -> IO ()
runIngest IngestOpts{..} = do
-- Validate: --dataset is required when --fit is used
when (ingestFit && null ingestDataset) $ do
hPutStrLn stderr "Error: --dataset is required when --fit is used"
exitFailure
let mds = if null ingestDataset then Nothing else Just ingestDataset
alg = ingestFormat
varnames = ingestVarnames
batchSize = 1000 :: Int
-- Open the expression file (or stdin)
h <- if null ingestExprs then pure stdin else openFile ingestExprs ReadMode
-- Open DB
putStrLn $ "Opening " ++ ingestDb ++ "..."
db <- open (T.pack ingestDb)
-- Process line by line, batch and insert
putStrLn "Processing expressions..."
totalRef <- newIORef (0 :: Int)
validRef <- newIORef (0 :: Int)
failedRef <- newIORef (0 :: Int)
classesRef <- newIORef (0 :: Int)
batchRef <- newIORef ([] :: [(Fix SRTree, [Target], Maybe Double)])
let flushBatch = do
batch <- readIORef batchRef
unless (null batch) $ do
r <- importEqs db mds (reverse batch)
case r of
Left err -> hPutStrLn stderr $ " BATCH INSERT FAILED: " ++ err
Right s -> modifyIORef' classesRef (+ isClasses s)
writeIORef batchRef []
processLine line = do
modifyIORef' totalRef (+1)
if null line
then pure ()
else case parseSR alg (B.pack varnames) False (B.pack line) of
Left err -> do
modifyIORef' failedRef (+1)
hPutStrLn stderr $ " FAILED: " ++ line ++ " -- " ++ err
Right tree -> do
modifyIORef' validRef (+1)
modifyIORef' batchRef ((relabelParams tree, [], Nothing) :)
batch <- readIORef batchRef
when (length batch >= batchSize) $ do
flushBatch
v <- readIORef validRef
hPutStrLn stderr $ " ... " ++ show v ++ " expressions processed"
hFlush stderr
loop = do
done <- hIsEOF h
if done then pure ()
else do
line <- hGetLine h
processLine line
loop
loop
flushBatch -- insert any remaining expressions
-- Close file handle
unless (null ingestExprs) (hClose h)
-- Summary
total <- readIORef totalRef
valid <- readIORef validRef
failed <- readIORef failedRef
classes <- readIORef classesRef
putStrLn $ "Parsed " ++ show total ++ " expressions ("
++ show valid ++ " valid, " ++ show failed ++ " failed)"
putStrLn $ "Imported into " ++ ingestDb
++ maybe "" (\d -> " [dataset: " ++ d ++ "]") mds
++ ": " ++ show classes ++ " e-classes"
close db
when (ingestEqsatSteps > 0) $
putStrLn "(eqsat after ingest not yet implemented in standalone CLI)"