packages feed

hanalyze-0.1.0.0: bench/haskell/BenchDataGen.hs

-- | Generate the shared benchmark CSV inputs that both Haskell and Python
-- benchmarks read.
--
-- Fixed seed (mwc-random initialised from a deterministic word vector) so
-- every machine produces byte-identical CSVs. Runs all generators
-- sequentially and writes to @bench/data/@.

module Main where

import qualified Data.Vector              as V
import qualified Data.Vector.Storable     as VS
import qualified Numeric.LinearAlgebra    as LA
import           System.Random.MWC        (initialize, GenIO, uniformR)
import           System.Random.MWC.Distributions (standard)
import           Control.Monad            (replicateM, forM_)
import           System.Directory         (createDirectoryIfMissing)
import           Text.Printf              (printf, hPrintf)
import           System.IO                (withFile, IOMode (..), Handle, hPutStrLn)

main :: IO ()
main = do
  createDirectoryIfMissing True "bench/data"

  -- ---- Regression scenarios (B1) -----------------------------------------
  -- LM/Ridge: y = X β + ε, β fixed, ε ~ N(0, 0.5²)
  forM_ [(1000, 5), (10000, 50), (100000, 100)] $ \(n, p) ->
    genLM ("bench/data/lm_n" ++ show n ++ "_p" ++ show p ++ ".csv") n p

  -- GLM Logistic
  forM_ [(2000, 10), (10000, 20)] $ \(n, p) ->
    genLogistic ("bench/data/logistic_n" ++ show n ++ "_p" ++ show p ++ ".csv") n p

  -- GLM Poisson
  forM_ [(2000, 10), (10000, 20)] $ \(n, p) ->
    genPoisson ("bench/data/poisson_n" ++ show n ++ "_p" ++ show p ++ ".csv") n p

  -- GLMM (random intercept by group)
  forM_ [(2000, 5, 20), (10000, 10, 50)] $ \(n, p, g) ->
    genGLMM ("bench/data/glmm_n" ++ show n ++ "_p" ++ show p
             ++ "_g" ++ show g ++ ".csv") n p g

  -- ---- Kernel / GP scenarios (B2) ---------------------------------------
  -- y = sin(x1) + 0.5 cos(x2) + 0.3 x3 + ε  (smooth, low-noise)
  forM_ [(500, 1), (500, 5), (1000, 5), (2000, 5), (4000, 5)] $ \(n, p) ->
    genKernel ("bench/data/kernel_n" ++ show n ++ "_p" ++ show p ++ ".csv") n p

  putStrLn "All bench/data CSVs generated."

-- ---------------------------------------------------------------------------
-- LM / Ridge
-- ---------------------------------------------------------------------------

-- | y = Xβ + ε, β = sin(j+1) / (j+1), ε ~ N(0, 0.5²)
genLM :: FilePath -> Int -> Int -> IO ()
genLM path n p = do
  gen <- mkGen "lm" n p
  rows <- replicateM n (replicateM p (standard gen))
  noise <- replicateM n (standard gen)
  let beta  = [ sin (fromIntegral j + 1) / (fromIntegral j + 1)
              | j <- [0 .. p - 1 :: Int] ]
      ys    = [ sum (zipWith (*) row beta) + 0.5 * eps
              | (row, eps) <- zip rows noise ]
  writeXY path p rows ys

-- ---------------------------------------------------------------------------
-- Logistic GLM
-- ---------------------------------------------------------------------------

genLogistic :: FilePath -> Int -> Int -> IO ()
genLogistic path n p = do
  gen <- mkGen "logistic" n p
  rows <- replicateM n (replicateM p (standard gen))
  let beta = [ 0.5 * sin (fromIntegral j + 1) | j <- [0 .. p - 1 :: Int] ]
      eta  = [ sum (zipWith (*) r beta) | r <- rows ]
      mu   = map (\e -> 1 / (1 + exp (- e))) eta
  ys <- mapM (\m -> do
                u <- uniformR (0, 1) gen :: IO Double
                return (if u < m then 1.0 else 0.0)) mu
  writeXY path p rows ys

-- ---------------------------------------------------------------------------
-- Poisson GLM
-- ---------------------------------------------------------------------------

genPoisson :: FilePath -> Int -> Int -> IO ()
genPoisson path n p = do
  gen <- mkGen "poisson" n p
  rows <- replicateM n (replicateM p (uniformR (-1.0, 1.0) gen :: IO Double))
  let beta = [ 0.3 * sin (fromIntegral j + 1) | j <- [0 .. p - 1 :: Int] ]
      eta  = [ 0.5 + sum (zipWith (*) r beta) | r <- rows ]
      mu   = map exp eta
  ys <- mapM (samplePoisson gen) mu
  writeXY path p rows (map fromIntegral (ys :: [Int]))

samplePoisson :: GenIO -> Double -> IO Int
samplePoisson g lam
  | lam < 30  = sampleSmallPoisson g lam
  | otherwise = do
      -- 正規近似で十分 (ベンチデータ生成なので exact は不要)
      z <- standard g
      return (max 0 (round (lam + sqrt lam * z)))

sampleSmallPoisson :: GenIO -> Double -> IO Int
sampleSmallPoisson g lam = go 0 1.0
  where
    el = exp (- lam)
    go k pAcc = do
      u <- uniformR (0, 1) g :: IO Double
      let pNew = pAcc * u
      if pNew <= el then return k
                    else go (k + 1) pNew

-- ---------------------------------------------------------------------------
-- GLMM (Gaussian, random intercept)
-- ---------------------------------------------------------------------------

-- | n 観測 / p fixed effects / g groups。各群に N(0, σ_u² = 1) の切片。
genGLMM :: FilePath -> Int -> Int -> Int -> IO ()
genGLMM path n p g = do
  gen <- mkGen "glmm" n (p + g)
  rows  <- replicateM n (replicateM p (standard gen))
  noise <- replicateM n (standard gen)
  uVec  <- replicateM g (standard gen)
  let beta   = [ 0.5 * sin (fromIntegral j + 1) | j <- [0 .. p - 1 :: Int] ]
      groups = [ i `mod` g | i <- [0 .. n - 1 :: Int] ]
      ys     = [ sum (zipWith (*) r beta)
                 + (uVec !! (groups !! i))
                 + 0.3 * eps
               | (i, (r, eps)) <- zip [0 ..] (zip rows noise) ]
  writeXYG path p rows groups ys

-- ---------------------------------------------------------------------------
-- Kernel / GP regression target
-- ---------------------------------------------------------------------------

-- | f(x) = sin(x1) + 0.5 cos(x2) + 0.3 x3 + ... + ε ~ N(0, 0.05²)
genKernel :: FilePath -> Int -> Int -> IO ()
genKernel path n p = do
  gen <- mkGen "kernel" n p
  rows <- replicateM n (replicateM p (uniformR (-3, 3) gen :: IO Double))
  noise <- replicateM n (standard gen)
  let f r = case r of
              []        -> 0
              [a]       -> sin a
              [a, b]    -> sin a + 0.5 * cos b
              (a:b:c:_) -> sin a + 0.5 * cos b + 0.3 * c
      ys  = zipWith (\r e -> f r + 0.05 * e) rows noise
  writeXY path p rows ys

-- ---------------------------------------------------------------------------
-- Helpers
-- ---------------------------------------------------------------------------

-- | Deterministic per-scenario seed: hash the tag + sizes into a Word32 vec.
mkGen :: String -> Int -> Int -> IO GenIO
mkGen tag n p =
  let seedInts = [ fromIntegral (length tag * 7919 + n * 31 + p)
                 , fromIntegral n
                 , fromIntegral p
                 , 0xDEADBEEF
                 ]
  in initialize (V.fromList seedInts)

writeXY :: FilePath -> Int -> [[Double]] -> [Double] -> IO ()
writeXY path p rows ys = withFile path WriteMode $ \h -> do
  let header = "x0" ++ concat [ "," ++ "x" ++ show j | j <- [1 .. p - 1] ]
                    ++ ",y"
  hPutStrLn h header
  mapM_ (\(r, y) -> do
            let cells = map dShow r ++ [dShow y]
            hPutStrLn h (intercalate1 "," cells)) (zip rows ys)
  printf "  wrote %s (%d × %d)\n" path (length rows) (p + 1)

writeXYG
  :: FilePath -> Int -> [[Double]] -> [Int] -> [Double] -> IO ()
writeXYG path p rows groups ys = withFile path WriteMode $ \h -> do
  let header = "x0" ++ concat [ "," ++ "x" ++ show j | j <- [1 .. p - 1] ]
                    ++ ",group,y"
  hPutStrLn h header
  mapM_ (\((r, g), y) -> do
            let cells = map dShow r ++ [show g, dShow y]
            hPutStrLn h (intercalate1 "," cells))
        (zip (zip rows groups) ys)
  printf "  wrote %s (%d × %d, %d groups)\n"
         path (length rows) (p + 2) (length (uniqueInts groups))

dShow :: Double -> String
dShow = printf "%.10g"

intercalate1 :: String -> [String] -> String
intercalate1 _   []     = ""
intercalate1 _   [x]    = x
intercalate1 sep (x:xs) = x ++ sep ++ intercalate1 sep xs

uniqueInts :: [Int] -> [Int]
uniqueInts = foldr (\x acc -> if x `elem` acc then acc else x : acc) []