packages feed

hanalyze-0.2.0.0: bench/posteriordb/15-dugongs/Model.hs

{-# LANGUAGE OverloadedStrings #-}
-- | dugongs_data-dugongs_model (posteriordb) — hanalyze (ModelP) 実装。
--
-- Phase 89: posteriordb 横断ベンチマーク。BUGS 古典例「ジュゴンの成長曲線」
-- (N=27頭・体長 Y と年齢 x の非線形漸近成長曲線回帰)。
--
-- Stan 原典 (posteriordb `models/stan/dugongs_model.stan`):
--   parameters {
--     real alpha; real beta;
--     real<lower=.5,upper=1> lambda;
--     real<lower=0> tau;
--   }
--   transformed parameters { sigma = 1/sqrt(tau); U3 = logit(lambda); }
--   model {
--     m[i] = alpha - beta * pow(lambda, x[i]);
--     Y ~ normal(m, sigma);
--     alpha ~ normal(0,1000); beta ~ normal(0,1000);
--     lambda ~ uniform(.5,1); tau ~ gamma(.0001,.0001);
--   }
--
-- 高レベル API (`df |-> hbm`) を使用。 sigma/U3 は log-density に寄与しない
-- transformed parameters なので `deterministic` (PyMC Deterministic 相当) で
-- 事後サンプルに注入する (14-hmm-example と同じパターン)。
--
-- ★実測で踏んだ罠: `lambda ~ Uniform(.5,1)` をそのまま `sample` すると
-- 10-rats で確認済みの罠が新形態で再現した — hanalyze の Uniform は
-- unconstrained 扱い (Distribution.hs:309-310) で unconstrained 初期値
-- raw=0 がそのまま lambda=0 になる (Uniform(lo,hi) は raw をそのまま
-- 値として使う・変換なし)。 lambda=0 は `Uniform(.5,1)` の台の外なので
-- 初手から `logDensity = -Infinity` となり、全 4 chain・全 warmup で HMC
-- 提案が拒否され続けて `alpha=beta=lambda=0.0000・tau=sigma=1.0000` に
-- 完全凍結する現象を実機で確認した (`ess=1000`・`r_hat=NA` は分散ゼロの
-- 兆候)。解決: `lambda ~ Uniform(.5,1)` を「`u ~ Beta(1,1)`
-- (= Uniform(0,1) と同一分布・`UnitIntervalT` 変換で真に (0,1) に収まる
-- 安全な初期値を持つ) → `lambda = 0.5 + 0.5*u`」というアフィン再パラメタ化
-- に置換 (Jacobian は定数 0.5 で HMC の相対密度に影響しないため厳密に
-- 等価)。 14-hmm-example の順序制約 (加算シフト+potential) と同系統の
-- 「unconstrained分布の代わりに真に制約された分布から affine 変換する」
-- 対処法。
--
-- reference_posterior_name = null (posteriordb に公式 reference posterior 無し)。
--
-- ビルド: cabal build --project-file=cabal.project.plot posteriordb-dugongs
module Main (main) where

import Control.Monad (unless)
import Data.Aeson (FromJSON (..), withObject, (.:), eitherDecodeFileStrict)
import qualified Data.Text as T
import qualified Data.Vector as V
import System.Environment (getArgs)
import Text.Printf (printf)

import Hanalyze.Model.HBM (ModelP, Distribution (..), sample, observe,
                                    dataNamedX, dataNamedObs, plateForM_,
                                    deterministic, augmentChainWithDeterministic)
import Hanalyze.Model.HBM (gradPathLabel)
import Hanalyze.Plot (hbmModelSpec)
import Hanalyze.Plot (HBMConfig (..), defaultHBM, hbm, (|->),
                              dashboardFullOf, hbmChainsR)
import Hgg.Plot.Spec (ColData (..))
import Hgg.Plot.Frame (BoundPlot, (|>>))
import Hgg.Plot.Backend.Rasterific (savePNGBound)

import Common (summarize, printSummary, timeSamplingMs)

-- | posteriordb の @dugongs_data.json@ 形状 ({"Y":[...], "x":[...], "N":27})。
data DugongsData = DugongsData
  { dugongsY :: [Double]
  , dugongsX :: [Double]
  }

instance FromJSON DugongsData where
  parseJSON = withObject "DugongsData" $ \v ->
    DugongsData <$> v .: "Y" <*> v .: "x"

noDf :: [(T.Text, ColData)]
noDf = []

dataPath :: FilePath
dataPath = "bench/posteriordb/15-dugongs/data/dugongs_data.json"

figuresDir :: FilePath
figuresDir = "bench/posteriordb/15-dugongs/figures"

readData :: IO ([Double], [Double])
readData = do
  d <- either fail pure =<< eitherDecodeFileStrict dataPath
  pure (dugongsY d, dugongsX d)

-- | 非線形漸近成長曲線回帰 (Stan 原典と同一構造)。
dugongsModel :: ModelP ()
dugongsModel = do
  alpha  <- sample "alpha"  (Normal 0 1000)
  beta   <- sample "beta"   (Normal 0 1000)
  u      <- sample "u"      (Beta 1 1)
  lambda <- deterministic "lambda" (0.5 + 0.5 * u)
  tau    <- sample "tau"    (Gamma 0.0001 0.0001)
  sigma  <- deterministic "sigma" (1 / sqrt tau)
  _      <- deterministic "U3" (log (lambda / (1 - lambda)))
  xs <- dataNamedX   "x" []
  ys <- dataNamedObs "Y" []
  plateForM_ "obs" (zip xs ys) $ \(xi, yi) ->
    observe "Y" (Normal (alpha - beta * (lambda ** xi)) sigma) [yi]

main :: IO ()
main = do
  (ys, xs) <- readData
  -- Phase 102 A1: `prof` 引数で図出力 skip (Rasterific が cost centre を
  -- 汚さないため)。本モデルは full でも sampling ~325ms と小さく、hmm の
  -- `reduced` (1chain 縮小) では prof tick が不足するためサンプリング設定は
  -- 本番のまま据え置く。
  args <- getArgs
  let profRun = elem "prof" args
  let df = [ ("x", NumData (V.fromList xs))
           , ("Y", NumData (V.fromList ys))
           ] :: [(T.Text, ColData)]
      -- PyMC 側 (model.py) と同じ設定を定数で揃える。
      cfg = defaultHBM { hbmChains = 4, hbmSamples = 1000
                        , hbmWarmup = 1000, hbmSeed = Just 1 }
      m = df |-> hbm cfg dugongsModel

  -- 勾配経路 = compileGradUV が実際に選ぶ経路 (束縛済 hbmModelSpec で判定・
  -- Phase 91 A4: 生モデルを synthVecIR に渡すと data 空で誤表示するため差替)。
  putStrLn $ "勾配経路 = " ++ gradPathLabel (hbmModelSpec m)

  (_, samplingMs) <- timeSamplingMs (hbmChainsR m)
  printf "sampling wall = %.1f ms (draws only, no dashboard/startup)\n" samplingMs

  unless profRun $
    savePNGBound (figuresDir ++ "/hs_dashboard_full.png") $
      (noDf |>> dashboardFullOf m "Y" :: BoundPlot)

  -- sigma/U3 は deterministic (log-density に寄与しない transformed
  -- parameters) のため、summarize の前に augmentChainWithDeterministic で
  -- Chain へ注入する (14-hmm-example と同じ理由)。
  let chainsAug = map (augmentChainWithDeterministic dugongsModel) (hbmChainsR m)
  printSummary $ summarize ["alpha", "beta", "lambda", "tau", "sigma"] chainsAug