srtree-3.0.0.3: apps/TestCI/Main.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Main where
import qualified Data.ByteString.Char8 as B
import Data.SRTree
import Data.SRTree.Print (showExpr)
import Data.SRTree.Eval (Target, Columns, compile)
import Data.SRTree.Datasets (loadDataset)
import Data.SRTree.Recursion (Fix(..))
import Algorithm.SRTree.ConfidenceIntervals
import Algorithm.SRTree.Compile (compileTree, EvalTree(..))
import Algorithm.SRTree.NonlinearOpt (minimizeNLL, minimizeNLLWith, compileLossAndGrad)
import Algorithm.SRTree.Likelihoods (Distribution(..), Loss(..))
import Algorithm.SRTree.AD (ADBackEnd(..))
import Algorithm.SRTree.AD.Unboxed (setMTPopParallel)
import Numeric.Optimization.NLOPT (LocalAlgorithm(..))
import Text.ParseSR (parseSR, SRAlgs(..))
import qualified Data.Vector.Unboxed as VU
import qualified Data.Vector.Storable as VS
import qualified Data.Vector.Generic as G
import Data.List (intercalate, foldl', maximumBy, isSuffixOf)
import Data.Maybe (fromMaybe)
import System.IO (hFlush, stdout, hPutStrLn, stderr)
import System.Environment (getArgs)
import System.Random (randomRIO)
import Control.Exception (catch, SomeException, evaluate)
import Control.Monad (forM_, when)
import Data.Either (isRight)
-- | A test case: expression string, description
data TestCase = TestCase
{ tcName :: String
, tcExpr :: String
, tcDesc :: String
} deriving (Show)
-- | All test cases
testCases :: [TestCase]
testCases =
[ TestCase
{ tcName = "exp-param"
, tcExpr = "Exp(t0 + (x1 * (t1 * ((x0 * x0) + t2))))"
, tcDesc = "Parameterized exponential (similar shape to AGENTS.md example)"
}
, TestCase
{ tcName = "linear"
, tcExpr = "t0 * x0 + t1"
, tcDesc = "Simple linear model (well-conditioned)"
}
, TestCase
{ tcName = "quadratic"
, tcExpr = "t0 * x0 * x0 + t1 * x0 + t2"
, tcDesc = "Quadratic polynomial"
}
, TestCase
{ tcName = "rational"
, tcExpr = "t0 / (t1 + x0)"
, tcDesc = "Rational function (steep near pole)"
}
, TestCase
{ tcName = "sine"
, tcExpr = "t0 * sin(t1 * x0 + t2)"
, tcDesc = "Sinusoidal model"
}
, TestCase
{ tcName = "product"
, tcExpr = "t0 * x0 * x1 + t1"
, tcDesc = "Two-variable product"
}
, TestCase
{ tcName = "exp-linear"
, tcExpr = "Exp(t0 * x0 + t1)"
, tcDesc = "Exponential of linear (simpler than deep exp)"
}
, TestCase
{ tcName = "power"
, tcExpr = "t0 * x0 ** t1"
, tcDesc = "Power law"
}
, TestCase
{ tcName = "linear-mse"
, tcExpr = "t0 * x0 + t1"
, tcDesc = "Linear model fitted with MSE (Bates 1985 original use case)"
}
]
main :: IO ()
main = do
args <- getArgs
let filterName = if null args then Nothing else Just (head args)
putStrLn "========================================================================"
putStrLn " Profile Likelihood CI Backend Investigation"
putStrLn "========================================================================"
putStrLn ""
-- Load dataset
let dataSpec = "../eggp/gaussian_train.csv:::y_noise_02:x1,x2"
putStrLn $ "Loading dataset: " ++ dataSpec
hFlush stdout
((xTr, yTr, _xVal, _yVal), (mYErr, _), _varnames, _target) <-
loadDataset dataSpec True `catch` (\(e :: SomeException) -> do
putStrLn $ "ERROR loading dataset: " ++ show e
error "Failed to load dataset")
let nSamples = VU.length yTr
putStrLn $ " Samples: " ++ show nSamples
putStrLn $ " Features: " ++ show (length xTr)
putStrLn ""
let cases = case filterName of
Nothing -> testCases
Just name -> filter (\tc -> tcName tc == name) testCases
mapM_ (runTestCase xTr yTr mYErr nSamples) cases
putStrLn ""
putStrLn "========================================================================"
putStrLn " Summary"
putStrLn "========================================================================"
putStrLn ""
putStrLn "Key observations:"
putStrLn " - Laplace: uses Hessian inverse; fast but may be inaccurate for nonlinear models"
putStrLn " - Bates: classical profile walk; accurate but slow"
putStrLn " - ODE: Chen & Jennrich ODE-based profile; fast and accurate"
putStrLn " - Constrained: bisection on re-optimized endpoints; fast but may fail"
putStrLn ""
putStrLn "Issues to investigate:"
putStrLn " 1. Constrained backend NaN on steep exponential expressions"
putStrLn " 2. tau_max' threshold correctness for NLL Gaussian"
putStrLn " 3. Nelder-Mead convergence in augmented Lagrangian"
putStrLn " 4. One-sided CI failures"
-- | Run a single test case through all backends
runTestCase :: [VU.Vector Double] -> VU.Vector Double -> Maybe (VU.Vector Double) -> Int -> TestCase -> IO ()
runTestCase xTr yTr mYErr nSamples tc = do
putStrLn "------------------------------------------------------------------------"
putStrLn $ "Test: " ++ tcName tc
putStrLn $ " Description: " ++ tcDesc tc
putStrLn $ " Expression: " ++ tcExpr tc
putStrLn ""
-- Parse expression: convert String to ByteString for parseSR
let parsed = parseSR TIR (B.pack "x0,x1") False (B.pack (tcExpr tc))
case parsed of
Left err -> putStrLn $ " PARSE ERROR: " ++ err
Right rawTree -> do
let tree = relabelParams rawTree
let nParams = countParamsUniq tree
putStrLn $ " Parsed tree: " ++ showExpr tree
putStrLn $ " Unique params: " ++ show nParams
if nParams == 0
then putStrLn " SKIP: no parameters to profile"
else do
-- Detect MSE cases (name ends with "-mse")
let useMSE = "-mse" `isSuffixOf` tcName tc
dist = if useMSE then LeastSquares else Gaussian
totalParams = if useMSE then nParams else nParams + 1 -- +1 for sigma when Gaussian
putStrLn $ " Loss: " ++ (if useMSE then "MSE (LeastSquares)" else "NLL Gaussian")
putStrLn $ " Total params: " ++ show totalParams
putStrLn ""
-- Fit with multiple restarts
putStrLn " Fitting..."
hFlush stdout
setMTPopParallel True
results <- fitMultipleRestarts dist mYErr xTr yTr tree totalParams 5
setMTPopParallel False
let (bestNLL, bestTheta) = maximumBy (\(a,_) (b,_) -> compare a b) results
theta_opt = bestTheta
negNLL = negate bestNLL
putStrLn $ " Best loss: " ++ show negNLL
putStrLn $ " Theta: " ++ show (VU.toList theta_opt)
putStrLn ""
-- Compile the EvalTree for CI computation
let et = compileTree dist xTr yTr mYErr tree
-- Verify the optimizer agrees
let theta_verify = ctOptimizer et theta_opt
nll_verify = ctNLL et theta_verify
putStrLn $ " Verified loss (via EvalTree): " ++ show nll_verify
-- Compute standard errors from Hessian
let stats = getStatsFromModel dist mYErr xTr yTr tree theta_opt
stdErrs = _stdErr stats
putStrLn $ " Std errors (Hessian): " ++ show (VU.toList stdErrs)
putStrLn ""
let paramNames = [ "t" ++ show i | i <- [0 .. nParams - 1] ]
++ if useMSE then [] else ["sigma"]
-- ---- LAPLACE ----
putStrLn " === LAPLACE ==="
let laplaceCI = paramCI (Laplace stats) nSamples theta_opt 0.05
putStrLn $ " 95% CIs:"
putStrLn $ " " ++ showCIList (zip paramNames laplaceCI)
putStrLn ""
-- ---- BATES (profile walk) ----
putStrLn " === BATES (profile walk) ==="
catch (do
let estCIs = laplaceCI
profiles_bates = getAllProfiles Bates et theta_opt stdErrs estCIs 0.05
batesCI = paramCI (Profile stats profiles_bates) nSamples theta_opt 0.05
putStrLn $ " 95% CIs:"
putStrLn $ " " ++ showCIList (zip paramNames batesCI)
putStrLn $ " Widths: " ++ show (map (\(CI _ l h) -> h - l) batesCI)
-- Debug: show first/last profile points
forM_ (zip [0::Int ..] profiles_bates) $ \(ix, prof) -> do
let taus = _taus prof
cols = _thetas prof
nT = VU.length taus
putStrLn $ " Profile t" ++ show ix ++ ": " ++ show nT ++ " points"
when (nT > 0) $ do
let firstTau = taus VU.! 0
lastTau = taus VU.! (nT - 1)
firstTh = (cols !! ix) VU.! 0
lastTh = (cols !! ix) VU.! (nT - 1)
optTh = theta_opt VU.! ix
putStrLn $ " tau=[" ++ show firstTau ++ ", " ++ show lastTau ++ "]"
putStrLn $ " theta=[" ++ show firstTh ++ ", " ++ show lastTh ++ "] opt=" ++ show optTh
) (\(e :: SomeException) -> putStrLn $ " ERROR: " ++ show e)
putStrLn ""
-- ---- ODE (Chen & Jennrich) ----
putStrLn " === ODE (Chen & Jennrich) ==="
catch (do
let estCIs = laplaceCI
profiles_ode = getAllProfiles ODE et theta_opt stdErrs estCIs 0.05
odeCI = paramCI (Profile stats profiles_ode) nSamples theta_opt 0.05
putStrLn $ " 95% CIs:"
putStrLn $ " " ++ showCIList (zip paramNames odeCI)
putStrLn $ " Widths: " ++ show (map (\(CI _ l h) -> h - l) odeCI)
) (\(e :: SomeException) -> putStrLn $ " ERROR: " ++ show e)
putStrLn ""
-- ---- CONSTRAINED ----
putStrLn " === CONSTRAINED (bisection) ==="
catch (do
let profiles_cnstr = getAllProfiles Constrained et theta_opt stdErrs [] 0.05
cnstrCI = paramCI (Profile stats profiles_cnstr) nSamples theta_opt 0.05
putStrLn $ " 95% CIs:"
putStrLn $ " " ++ showCIList (zip paramNames cnstrCI)
putStrLn $ " Widths: " ++ show (map (\(CI _ l h) -> h - l) cnstrCI)
-- Check for NaN
let hasNaN = any (\(CI _ l h) -> isNaN l || isNaN h) cnstrCI
when hasNaN $ putStrLn $ " *** WARNING: NaN detected in Constrained CI ***"
) (\(e :: SomeException) -> putStrLn $ " ERROR: " ++ show e)
putStrLn ""
-- ---- Detailed profiling of Constrained for the problematic case ----
when (nParams >= 2) $ do
putStrLn " === DETAILED CONSTRAINED INVESTIGATION ==="
investigateConstrained et theta_opt stdErrs totalParams
putStrLn ""
-- | Fit with multiple random restarts
fitMultipleRestarts :: Distribution -> Maybe (VU.Vector Double) -> [VU.Vector Double] -> VU.Vector Double
-> Fix SRTree -> Int -> Int -> IO [(Double, VU.Vector Double)]
fitMultipleRestarts dist mYErr xTr yTr tree nParams nRep = do
let funAndGrad = compileLossAndGrad MultiThread (NLL dist) mYErr xTr yTr tree
runRestart = do
theta0 <- VU.replicateM nParams (randomRIO (-2, 2))
let (theta, lossVal, _) = minimizeNLLWith funAndGrad TNEWTON 200 theta0
pure (negate lossVal, theta)
results <- sequence [ runRestart | _ <- [1..nRep] ]
-- Also try from zeros
let theta0_zero = VU.replicate nParams 0.0
(theta_zero, loss_zero, _) = minimizeNLLWith funAndGrad TNEWTON 200 theta0_zero
pure $ (negate loss_zero, theta_zero) : results
-- | Show a list of CIs with parameter names
showCIList :: [(String, CI)] -> String
showCIList = intercalate "\n " . map (\(name, CI est lo hi) ->
name ++ ": " ++ showF lo ++ " <= " ++ showF est ++ " <= " ++ showF hi)
where showF x
| isNaN x = "NaN"
| isInfinite x = if x > 0 then "+Inf" else "-Inf"
| otherwise = show (fromIntegral (round (x * 1e4) :: Int) / 1e4 :: Double)
-- | Detailed investigation of the Constrained backend
investigateConstrained :: EvalTree -> VU.Vector Double -> VU.Vector Double -> Int -> IO ()
investigateConstrained et theta_opt stdErrs nParams = do
let nll_opt = ctNLL et theta_opt
n = ctRows et
k = VU.length theta_opt
chi2_1 = 3.841 -- chi2 quantile for 1 df at 0.95
putStrLn $ " nll_opt = " ++ show nll_opt
putStrLn $ " n = " ++ show n ++ ", k = " ++ show k
putStrLn $ " chi2_1(0.95) = " ++ show chi2_1
putStrLn ""
-- Corrected tau_max' calculation
let tau_max' = chi2_1 / 2
tau_max_old = nll_opt * chi2_1 / fromIntegral n -- OLD (buggy)
putStrLn $ " Corrected tau_max' (chi2_1/2) = " ++ show tau_max'
putStrLn $ " OLD tau_max' (nll_opt * chi2_1 / n) = " ++ show tau_max_old ++ " (WRONG)"
putStrLn ""
-- Test each parameter
forM_ [0 .. nParams - 1] $ \ix -> do
putStrLn $ " Parameter t" ++ show ix ++ ":"
putStrLn $ " MLE = " ++ show (theta_opt VU.! ix)
putStrLn $ " StdErr = " ++ show (stdErrs VU.! ix)
-- Test getEndPoint directly
let getPoint isLeft = getEndPoint et theta_opt tau_max' (stdErrs VU.! ix) ix isLeft
catch (do
let leftPt = getPoint True
rightPt = getPoint False
putStrLn $ " Left endpoint = " ++ show leftPt
putStrLn $ " Right endpoint = " ++ show rightPt
when (isNaN leftPt || isNaN rightPt) $
putStrLn $ " *** NaN detected! ***"
when (leftPt > rightPt) $
putStrLn $ " *** Left > Right: reversed interval! ***"
) (\(e :: SomeException) -> putStrLn $ " ERROR in getEndPoint: " ++ show e)
-- Test the profiling function (fix ix, re-optimize others)
putStrLn $ " Testing ctOptimizerFixed..."
catch (do
let delta = stdErrs VU.! ix * 0.5
theta_left = VU.generate nParams (\j -> if j == ix then (theta_opt VU.! ix) - delta else theta_opt VU.! j)
theta_right = VU.generate nParams (\j -> if j == ix then (theta_opt VU.! ix) + delta else theta_opt VU.! j)
reopt_left = ctOptimizerFixed et ix theta_left
reopt_right = ctOptimizerFixed et ix theta_right
nll_left = ctNLL et reopt_left
nll_right = ctNLL et reopt_right
putStrLn $ " theta_left (fixed at " ++ show (theta_opt VU.! ix - delta) ++ ") -> reopt NLL = " ++ show nll_left
putStrLn $ " theta_right (fixed at " ++ show (theta_opt VU.! ix + delta) ++ ") -> reopt NLL = " ++ show nll_right
putStrLn $ " NLL increase left: " ++ show (nll_left - nll_opt)
putStrLn $ " NLL increase right: " ++ show (nll_right - nll_opt)
when (isNaN nll_left || isNaN nll_right) $
putStrLn $ " *** NaN in re-optimized NLL! ***"
) (\(e :: SomeException) -> putStrLn $ " ERROR in ctOptimizerFixed: " ++ show e)
putStrLn ""